diff --git a/.importlinter b/.importlinter index 31c8c359..075cae94 100644 --- a/.importlinter +++ b/.importlinter @@ -5,32 +5,27 @@ include_external_packages = True [importlinter:contract:1] name = Core modules do not import command modules type = forbidden +; A command's private run-logic now lives inside its own package +; (aai_cli/commands//_exec.py, …) and is governed by contract 2's +; independence rule, so those modules are intentionally absent here. The +; *_exec modules that remain are the ones still at the package root because +; they're shared beyond their command (transcribe_exec/render/batch and +; init_exec are reused by the onboarding wizard; setup_exec/doctor_checks too). source_modules = aai_cli.agent - aai_cli.agent_exec aai_cli.argscan aai_cli.auth - aai_cli.caption_exec aai_cli.choices aai_cli.client - aai_cli.clip_exec - aai_cli.clip_select aai_cli.code_gen aai_cli.coding_agent aai_cli.config aai_cli.config_builder aai_cli.context aai_cli.debuglog - aai_cli.deploy_exec - aai_cli.dev_exec - aai_cli.dictate_exec aai_cli.doctor_checks - aai_cli.dub_exec aai_cli.environments aai_cli.errors - aai_cli.eval_data - aai_cli.eval_hf_api - aai_cli.evaluate_exec aai_cli.follow aai_cli.help_panels aai_cli.help_text @@ -39,7 +34,6 @@ source_modules = aai_cli.init_exec aai_cli.jsonshape aai_cli.llm - aai_cli.llm_exec aai_cli.mediafile aai_cli.microphone aai_cli.onboard @@ -49,11 +43,8 @@ source_modules = aai_cli.remotefs aai_cli.render aai_cli.setup_exec - aai_cli.share_exec - aai_cli.speak_exec aai_cli.stdio aai_cli.steps - aai_cli.stream_exec aai_cli.streaming aai_cli.sync_stt aai_cli.telemetry @@ -65,7 +56,6 @@ source_modules = aai_cli.tts aai_cli.typer_patches aai_cli.update_check - aai_cli.webhook_listen aai_cli.wer aai_cli.ws aai_cli.youtube @@ -87,13 +77,13 @@ type = forbidden source_modules = aai_cli.argscan aai_cli.client - aai_cli.clip_select + aai_cli.commands.clip._select + aai_cli.commands.evaluate._data aai_cli.config aai_cli.config_builder aai_cli.debuglog aai_cli.environments aai_cli.errors - aai_cli.eval_data aai_cli.hotkey aai_cli.llm aai_cli.remotefs diff --git a/aai_cli/AGENTS.md b/aai_cli/AGENTS.md index f740bceb..1a7191fc 100644 --- a/aai_cli/AGENTS.md +++ b/aai_cli/AGENTS.md @@ -16,12 +16,30 @@ pipe → exit 0). ### Command layer & the registration convention -Each file in `aai_cli/commands/` is a Typer sub-app (`transcribe`, `stream`, +Each entry under `aai_cli/commands/` is a Typer sub-app (`transcribe`, `stream`, `dictate`, `agent`, `speak`, `llm`, `clip`, `dub`, `caption`, `eval`, `transcripts`, `login` (login/logout/whoami), `doctor`, `init`, `dev`, `share`, `deploy`, `setup`, `onboard`, `account` (balance/usage/limits), `keys`, `sessions`, `audit`, `telemetry` (status/enable/disable), `webhooks` (listen)). +**A command is either a single module *or* a package** — `command_registry` +discovers both (it iterates `pkgutil.iter_modules`, which enumerates packages +too). A simple command stays a flat `commands/.py`. A command with private +run-logic becomes a package `commands//`: `__init__.py` holds the Typer +`app` + `SPEC` (and is what gets imported as `aai_cli.commands.`), and its +support modules sit beside it **underscore-prefixed** — `_exec.py` for the +`run_` body, plus any private helpers (`clip/_select.py`, +`evaluate/_data.py`, `evaluate/_hf_api.py`). The underscore both marks them +private and avoids colliding with the package's own command functions (the +`webhooks` package binds a `listen` command, so its module is `_listen.py`, not +`listen.py`). This is the Prefect/spaCy convention: flat file by default, +promote to a folder only when the command has earned multiple modules. Run-logic +that's **shared beyond one command stays at the package root**, not inside a +command package — `transcribe_exec`/`transcribe_render`/`transcribe_batch` and +`init_exec` are reused by the onboarding wizard (`onboard/sections.py`), so they +live at the root alongside `doctor_checks`/`setup_exec` rather than under +`commands/transcribe/` or `commands/init/`. + **Adding a command is purely additive — no shared file edits.** Every command module declares a module-level `SPEC = command_registry.CommandModuleSpec(panel=…, order=…, commands=…)`: @@ -61,9 +79,10 @@ command module from another. function only parses argv into a frozen `Options` dataclass and hands it to a module-level `run_(opts, state, *, json_mode)` through a thin lambda adapter in `run_command(ctx, ..., json=...)`. The run commands follow it — -`aai_cli/stream_exec.py` (the reference implementation), `transcribe_exec.py`, -`agent_exec.py`, `speak_exec.py`, `llm_exec.py`, `clip_exec.py`, -`dictate_exec.py`. Because the run path is a plain function of data, tests +`commands/stream/_exec.py` (the reference implementation), `transcribe_exec.py` +(at the root — shared with onboarding), `commands/agent/_exec.py`, +`commands/speak/_exec.py`, `commands/llm/_exec.py`, `commands/clip/_exec.py`, +`commands/dictate/_exec.py`. Because the run path is a plain function of data, tests construct options directly (`dataclasses.replace` off a defaults instance, see `tests/test_stream_exec.py` and `tests/test_command_options_seam.py`) instead of round-tripping argv through `CliRunner` — which is also the cheap way to diff --git a/aai_cli/commands/agent.py b/aai_cli/commands/agent/__init__.py similarity index 96% rename from aai_cli/commands/agent.py rename to aai_cli/commands/agent/__init__.py index 7e2edbd8..6a92cffb 100644 --- a/aai_cli/commands/agent.py +++ b/aai_cli/commands/agent/__init__.py @@ -4,7 +4,7 @@ import typer -from aai_cli import agent_exec, choices, command_registry, help_panels, options, output +from aai_cli import choices, command_registry, help_panels, options, output from aai_cli.agent.session import DEFAULT_GREETING, DEFAULT_PROMPT from aai_cli.agent.voices import ( DEFAULT_VOICE, @@ -12,6 +12,7 @@ complete_voice, format_voice_list, ) +from aai_cli.commands.agent import _exec as agent_exec from aai_cli.context import AppState, run_command from aai_cli.help_text import examples_epilog diff --git a/aai_cli/agent_exec.py b/aai_cli/commands/agent/_exec.py similarity index 98% rename from aai_cli/agent_exec.py rename to aai_cli/commands/agent/_exec.py index 8d8b2cdc..9cf50474 100644 --- a/aai_cli/agent_exec.py +++ b/aai_cli/commands/agent/_exec.py @@ -1,6 +1,6 @@ """Run logic for `assembly agent`: the options/run split (see AGENTS.md). -The command module (aai_cli/commands/agent.py) only parses argv — it builds an +The command module (aai_cli/commands/agent/__init__.py) only parses argv — it builds an ``AgentOptions`` and hands it to ``run_agent`` via ``context.run_command``, so tests can drive validation, --show-code, and session wiring by constructing options directly, with no CliRunner argv round-trip. diff --git a/aai_cli/commands/caption.py b/aai_cli/commands/caption/__init__.py similarity index 96% rename from aai_cli/commands/caption.py rename to aai_cli/commands/caption/__init__.py index a6d3e694..581148fb 100644 --- a/aai_cli/commands/caption.py +++ b/aai_cli/commands/caption/__init__.py @@ -4,7 +4,8 @@ import typer -from aai_cli import caption_exec, command_registry, help_panels, options +from aai_cli import command_registry, help_panels, options +from aai_cli.commands.caption import _exec as caption_exec from aai_cli.context import run_command from aai_cli.help_text import examples_epilog diff --git a/aai_cli/caption_exec.py b/aai_cli/commands/caption/_exec.py similarity index 98% rename from aai_cli/caption_exec.py rename to aai_cli/commands/caption/_exec.py index cbe25442..53a628ea 100644 --- a/aai_cli/caption_exec.py +++ b/aai_cli/commands/caption/_exec.py @@ -1,6 +1,6 @@ """Run logic for `assembly caption`: transcribe → SRT export → ffmpeg burn-in. -The command module (aai_cli/commands/caption.py) only parses argv — it builds a +The command module (aai_cli/commands/caption/__init__.py) only parses argv — it builds a ``CaptionOptions`` and hands it to ``run_caption`` via ``context.run_command`` (the options/run split, see AGENTS.md), so tests drive the whole pipeline by constructing options directly. diff --git a/aai_cli/commands/clip.py b/aai_cli/commands/clip/__init__.py similarity index 97% rename from aai_cli/commands/clip.py rename to aai_cli/commands/clip/__init__.py index cd4d5ad6..934c8613 100644 --- a/aai_cli/commands/clip.py +++ b/aai_cli/commands/clip/__init__.py @@ -4,7 +4,8 @@ import typer -from aai_cli import clip_exec, command_registry, help_panels, llm, options +from aai_cli import command_registry, help_panels, llm, options +from aai_cli.commands.clip import _exec as clip_exec from aai_cli.context import run_command from aai_cli.help_text import examples_epilog diff --git a/aai_cli/clip_exec.py b/aai_cli/commands/clip/_exec.py similarity index 98% rename from aai_cli/clip_exec.py rename to aai_cli/commands/clip/_exec.py index 14ff69c5..aa8819fa 100644 --- a/aai_cli/clip_exec.py +++ b/aai_cli/commands/clip/_exec.py @@ -1,6 +1,6 @@ """Run logic for `assembly clip`: cut a media file by transcript content. -The command module (aai_cli/commands/clip.py) only parses argv — it builds a +The command module (aai_cli/commands/clip/__init__.py) only parses argv — it builds a ``ClipOptions`` and hands it to ``run_clip`` via ``context.run_command`` (the options/run split, see AGENTS.md), so tests drive transcript resolution and the ffmpeg orchestration by constructing options directly. The pure selection logic @@ -27,8 +27,9 @@ from rich.markup import escape -from aai_cli import clip_select, jsonshape, llm, mediafile, output, stdio, youtube -from aai_cli.clip_select import Segment +from aai_cli import jsonshape, llm, mediafile, output, stdio, youtube +from aai_cli.commands.clip import _select as clip_select +from aai_cli.commands.clip._select import Segment from aai_cli.context import AppState from aai_cli.errors import CLIError, UsageError diff --git a/aai_cli/clip_select.py b/aai_cli/commands/clip/_select.py similarity index 100% rename from aai_cli/clip_select.py rename to aai_cli/commands/clip/_select.py diff --git a/aai_cli/commands/deploy.py b/aai_cli/commands/deploy/__init__.py similarity index 92% rename from aai_cli/commands/deploy.py rename to aai_cli/commands/deploy/__init__.py index 6f3745b9..e8078d12 100644 --- a/aai_cli/commands/deploy.py +++ b/aai_cli/commands/deploy/__init__.py @@ -1,9 +1,10 @@ -# aai_cli/commands/deploy.py +# aai_cli/commands/deploy/__init__.py from __future__ import annotations import typer -from aai_cli import command_registry, deploy_exec, help_panels, options +from aai_cli import command_registry, help_panels, options +from aai_cli.commands.deploy import _exec as deploy_exec from aai_cli.context import run_command from aai_cli.help_text import examples_epilog diff --git a/aai_cli/deploy_exec.py b/aai_cli/commands/deploy/_exec.py similarity index 98% rename from aai_cli/deploy_exec.py rename to aai_cli/commands/deploy/_exec.py index f99f468b..5413d0c1 100644 --- a/aai_cli/deploy_exec.py +++ b/aai_cli/commands/deploy/_exec.py @@ -1,6 +1,6 @@ """Run logic for `assembly deploy`: ship the current project to a PaaS target. -The command module (aai_cli/commands/deploy.py) only parses argv — it builds a +The command module (aai_cli/commands/deploy/__init__.py) only parses argv — it builds a ``DeployOptions`` and hands it to ``run_deploy`` via ``context.run_command`` (the options/run split, see AGENTS.md), so tests drive target resolution and the subprocess orchestration by constructing options directly instead of diff --git a/aai_cli/commands/dev.py b/aai_cli/commands/dev/__init__.py similarity index 93% rename from aai_cli/commands/dev.py rename to aai_cli/commands/dev/__init__.py index 5d19eb7e..6446d542 100644 --- a/aai_cli/commands/dev.py +++ b/aai_cli/commands/dev/__init__.py @@ -1,9 +1,10 @@ -# aai_cli/commands/dev.py +# aai_cli/commands/dev/__init__.py from __future__ import annotations import typer -from aai_cli import command_registry, dev_exec, help_panels, options +from aai_cli import command_registry, help_panels, options +from aai_cli.commands.dev import _exec as dev_exec from aai_cli.context import run_command from aai_cli.help_text import examples_epilog from aai_cli.init import devserver diff --git a/aai_cli/dev_exec.py b/aai_cli/commands/dev/_exec.py similarity index 96% rename from aai_cli/dev_exec.py rename to aai_cli/commands/dev/_exec.py index a683bdbc..68cbe26e 100644 --- a/aai_cli/dev_exec.py +++ b/aai_cli/commands/dev/_exec.py @@ -1,6 +1,6 @@ """Run logic for `assembly dev`: boot the scaffolded project's dev server. -The command module (aai_cli/commands/dev.py) only parses argv — it builds a +The command module (aai_cli/commands/dev/__init__.py) only parses argv — it builds a ``DevOptions`` and hands it to ``run_dev`` via ``context.run_command`` (the options/run split, see AGENTS.md), so tests drive the server orchestration by constructing options directly instead of round-tripping argv. diff --git a/aai_cli/commands/dictate.py b/aai_cli/commands/dictate/__init__.py similarity index 95% rename from aai_cli/commands/dictate.py rename to aai_cli/commands/dictate/__init__.py index 32e166e9..5e55dd46 100644 --- a/aai_cli/commands/dictate.py +++ b/aai_cli/commands/dictate/__init__.py @@ -2,7 +2,8 @@ import typer -from aai_cli import command_registry, dictate_exec, help_panels, options +from aai_cli import command_registry, help_panels, options +from aai_cli.commands.dictate import _exec as dictate_exec from aai_cli.context import run_command from aai_cli.help_text import examples_epilog from aai_cli.sync_stt import MAX_AUDIO_SECONDS diff --git a/aai_cli/dictate_exec.py b/aai_cli/commands/dictate/_exec.py similarity index 98% rename from aai_cli/dictate_exec.py rename to aai_cli/commands/dictate/_exec.py index e33c81c3..0bd6d7f9 100644 --- a/aai_cli/dictate_exec.py +++ b/aai_cli/commands/dictate/_exec.py @@ -3,7 +3,7 @@ Push-to-talk dictation over the Sync STT API: wait for a hotkey, record the microphone until the hotkey is pressed again (or the duration cap), POST the utterance to the Sync API, print the transcript, repeat. The command module -(aai_cli/commands/dictate.py) only parses argv into a ``DictateOptions``; tests +(aai_cli/commands/dictate/__init__.py) only parses argv into a ``DictateOptions``; tests drive the session by constructing options directly and injecting the key/mic/ HTTP boundaries, with no CliRunner argv round-trip and no real terminal. """ diff --git a/aai_cli/commands/dub.py b/aai_cli/commands/dub/__init__.py similarity index 97% rename from aai_cli/commands/dub.py rename to aai_cli/commands/dub/__init__.py index d6271b9e..e0de15c5 100644 --- a/aai_cli/commands/dub.py +++ b/aai_cli/commands/dub/__init__.py @@ -4,7 +4,8 @@ import typer -from aai_cli import command_registry, dub_exec, help_panels, llm, options +from aai_cli import command_registry, help_panels, llm, options +from aai_cli.commands.dub import _exec as dub_exec from aai_cli.context import run_command from aai_cli.help_text import examples_epilog diff --git a/aai_cli/dub_exec.py b/aai_cli/commands/dub/_exec.py similarity index 99% rename from aai_cli/dub_exec.py rename to aai_cli/commands/dub/_exec.py index 14df95a1..617c031b 100644 --- a/aai_cli/dub_exec.py +++ b/aai_cli/commands/dub/_exec.py @@ -1,6 +1,6 @@ """Run logic for `assembly dub`: transcribe → translate → synthesize → ffmpeg track-swap. -The command module (aai_cli/commands/dub.py) only parses argv — it builds a +The command module (aai_cli/commands/dub/__init__.py) only parses argv — it builds a ``DubOptions`` and hands it to ``run_dub`` via ``context.run_command`` (the options/run split, see AGENTS.md), so tests drive the whole pipeline by constructing options directly. diff --git a/aai_cli/commands/evaluate.py b/aai_cli/commands/evaluate/__init__.py similarity index 94% rename from aai_cli/commands/evaluate.py rename to aai_cli/commands/evaluate/__init__.py index 69830d4c..94ee3562 100644 --- a/aai_cli/commands/evaluate.py +++ b/aai_cli/commands/evaluate/__init__.py @@ -2,16 +2,17 @@ The module is named ``evaluate`` because importing a module named ``eval`` would shadow the builtin; the command itself registers as ``eval``. The scoring/render -logic lives in ``aai_cli.evaluate_exec`` (the options/run split, see AGENTS.md). +logic lives in ``aai_cli.commands.evaluate._exec`` (the options/run split, see AGENTS.md). """ from __future__ import annotations import typer -from aai_cli import command_registry, evaluate_exec, help_panels, options +from aai_cli import command_registry, help_panels, options +from aai_cli.commands.evaluate import _exec as evaluate_exec +from aai_cli.commands.evaluate._exec import EvalSpeechModel from aai_cli.context import run_command -from aai_cli.evaluate_exec import EvalSpeechModel from aai_cli.help_text import examples_epilog app = typer.Typer() diff --git a/aai_cli/eval_data.py b/aai_cli/commands/evaluate/_data.py similarity index 99% rename from aai_cli/eval_data.py rename to aai_cli/commands/evaluate/_data.py index 6bb9808b..62f2afd7 100644 --- a/aai_cli/eval_data.py +++ b/aai_cli/commands/evaluate/_data.py @@ -21,7 +21,8 @@ from dataclasses import dataclass from pathlib import Path -from aai_cli import eval_hf_api, jsonshape, wer +from aai_cli import jsonshape, wer +from aai_cli.commands.evaluate import _hf_api as eval_hf_api from aai_cli.errors import APIError, CLIError, UsageError _MANIFEST_SUFFIXES = (".csv", ".jsonl") diff --git a/aai_cli/evaluate_exec.py b/aai_cli/commands/evaluate/_exec.py similarity index 97% rename from aai_cli/evaluate_exec.py rename to aai_cli/commands/evaluate/_exec.py index aa4fe8a9..08c23438 100644 --- a/aai_cli/evaluate_exec.py +++ b/aai_cli/commands/evaluate/_exec.py @@ -1,6 +1,6 @@ """Run logic for `assembly eval`: transcribe a dataset and score WER against references. -The command module (aai_cli/commands/evaluate.py) only parses argv — it builds an +The command module (aai_cli/commands/evaluate/__init__.py) only parses argv — it builds an ``EvalOptions`` and hands it to ``run_evaluate`` via ``context.run_command`` (the options/run split, see AGENTS.md), so tests drive the scoring and rendering by constructing options directly instead of round-tripping argv. @@ -19,7 +19,8 @@ import assemblyai as aai from rich.console import RenderableType -from aai_cli import client, eval_data, jsonshape, output, wer +from aai_cli import client, jsonshape, output, wer +from aai_cli.commands.evaluate import _data as eval_data from aai_cli.context import AppState from aai_cli.errors import CLIError, NotAuthenticated diff --git a/aai_cli/eval_hf_api.py b/aai_cli/commands/evaluate/_hf_api.py similarity index 100% rename from aai_cli/eval_hf_api.py rename to aai_cli/commands/evaluate/_hf_api.py diff --git a/aai_cli/commands/llm.py b/aai_cli/commands/llm/__init__.py similarity index 97% rename from aai_cli/commands/llm.py rename to aai_cli/commands/llm/__init__.py index e1f43075..eb4025af 100644 --- a/aai_cli/commands/llm.py +++ b/aai_cli/commands/llm/__init__.py @@ -2,8 +2,9 @@ import typer -from aai_cli import choices, command_registry, help_panels, llm_exec, options, output +from aai_cli import choices, command_registry, help_panels, options, output from aai_cli import llm as gateway +from aai_cli.commands.llm import _exec as llm_exec from aai_cli.context import run_command from aai_cli.errors import UsageError from aai_cli.help_text import examples_epilog diff --git a/aai_cli/llm_exec.py b/aai_cli/commands/llm/_exec.py similarity index 98% rename from aai_cli/llm_exec.py rename to aai_cli/commands/llm/_exec.py index 6fcb332b..798521da 100644 --- a/aai_cli/llm_exec.py +++ b/aai_cli/commands/llm/_exec.py @@ -1,6 +1,6 @@ """Run logic for `assembly llm`: the options/run split (see AGENTS.md). -The command module (aai_cli/commands/llm.py) only parses argv — it builds an +The command module (aai_cli/commands/llm/__init__.py) only parses argv — it builds an ``LlmOptions`` and hands it to ``run_llm`` via ``context.run_command``, so tests can drive one-shot and --follow behavior by constructing options directly, with no CliRunner argv round-trip. (``aai_cli/llm.py`` is the gateway client itself and is diff --git a/aai_cli/commands/share.py b/aai_cli/commands/share/__init__.py similarity index 91% rename from aai_cli/commands/share.py rename to aai_cli/commands/share/__init__.py index d8bba6bb..0e1eb05a 100644 --- a/aai_cli/commands/share.py +++ b/aai_cli/commands/share/__init__.py @@ -1,9 +1,10 @@ -# aai_cli/commands/share.py +# aai_cli/commands/share/__init__.py from __future__ import annotations import typer -from aai_cli import command_registry, help_panels, options, share_exec +from aai_cli import command_registry, help_panels, options +from aai_cli.commands.share import _exec as share_exec from aai_cli.context import run_command from aai_cli.help_text import examples_epilog diff --git a/aai_cli/share_exec.py b/aai_cli/commands/share/_exec.py similarity index 97% rename from aai_cli/share_exec.py rename to aai_cli/commands/share/_exec.py index c284db6a..3e51e651 100644 --- a/aai_cli/share_exec.py +++ b/aai_cli/commands/share/_exec.py @@ -1,6 +1,6 @@ """Run logic for `assembly share`: expose the local dev server on a public URL. -The command module (aai_cli/commands/share.py) only parses argv — it builds a +The command module (aai_cli/commands/share/__init__.py) only parses argv — it builds a ``ShareOptions`` and hands it to ``run_share`` via ``context.run_command`` (the options/run split, see AGENTS.md), so tests drive the tunnel orchestration by constructing options directly instead of round-tripping argv. diff --git a/aai_cli/commands/speak.py b/aai_cli/commands/speak/__init__.py similarity index 94% rename from aai_cli/commands/speak.py rename to aai_cli/commands/speak/__init__.py index 0aa7d768..f267c9dc 100644 --- a/aai_cli/commands/speak.py +++ b/aai_cli/commands/speak/__init__.py @@ -4,10 +4,11 @@ import typer -from aai_cli import command_registry, help_panels, options, speak_exec +from aai_cli import command_registry, help_panels, options +from aai_cli.commands.speak import _exec as speak_exec +from aai_cli.commands.speak._exec import DEFAULT_LANGUAGE from aai_cli.context import run_command from aai_cli.help_text import examples_epilog -from aai_cli.speak_exec import DEFAULT_LANGUAGE app = typer.Typer() diff --git a/aai_cli/speak_exec.py b/aai_cli/commands/speak/_exec.py similarity index 98% rename from aai_cli/speak_exec.py rename to aai_cli/commands/speak/_exec.py index 6b9059f4..0463fd16 100644 --- a/aai_cli/speak_exec.py +++ b/aai_cli/commands/speak/_exec.py @@ -1,6 +1,6 @@ """Run logic for `assembly speak`: the options/run split (see AGENTS.md). -The command module (aai_cli/commands/speak.py) only parses argv — it builds a +The command module (aai_cli/commands/speak/__init__.py) only parses argv — it builds a ``SpeakOptions`` and hands it to ``run_speak`` via ``context.run_command``, so tests can drive text resolution, voice assignment, and synthesis wiring by constructing options directly, with no CliRunner argv round-trip. diff --git a/aai_cli/commands/stream.py b/aai_cli/commands/stream/__init__.py similarity index 99% rename from aai_cli/commands/stream.py rename to aai_cli/commands/stream/__init__.py index b90bbc6b..149d5edc 100644 --- a/aai_cli/commands/stream.py +++ b/aai_cli/commands/stream/__init__.py @@ -5,7 +5,8 @@ import typer from assemblyai.streaming.v3 import Encoding, NoiseSuppressionModel, SpeechModel -from aai_cli import choices, command_registry, help_panels, llm, options, stream_exec +from aai_cli import choices, command_registry, help_panels, llm, options +from aai_cli.commands.stream import _exec as stream_exec from aai_cli.context import run_command from aai_cli.help_text import examples_epilog diff --git a/aai_cli/stream_exec.py b/aai_cli/commands/stream/_exec.py similarity index 99% rename from aai_cli/stream_exec.py rename to aai_cli/commands/stream/_exec.py index aa3e03e2..387592f9 100644 --- a/aai_cli/stream_exec.py +++ b/aai_cli/commands/stream/_exec.py @@ -1,6 +1,6 @@ """Run logic for `assembly stream`: a gh-style options/run split. -The command module (aai_cli/commands/stream.py) only parses argv — it builds a +The command module (aai_cli/commands/stream/__init__.py) only parses argv — it builds a ``StreamOptions`` and hands it to ``run_stream`` via ``context.run_command``. Keeping the run path a module-level function of plain data (instead of a closure over the Typer locals) lets tests drive validation, --show-code, and session wiring by diff --git a/aai_cli/commands/webhooks.py b/aai_cli/commands/webhooks/__init__.py similarity index 94% rename from aai_cli/commands/webhooks.py rename to aai_cli/commands/webhooks/__init__.py index a3d69c43..aabad518 100644 --- a/aai_cli/commands/webhooks.py +++ b/aai_cli/commands/webhooks/__init__.py @@ -1,9 +1,10 @@ -# aai_cli/commands/webhooks.py +# aai_cli/commands/webhooks/__init__.py from __future__ import annotations import typer -from aai_cli import command_registry, help_panels, options, webhook_listen +from aai_cli import command_registry, help_panels, options +from aai_cli.commands.webhooks import _listen as webhook_listen from aai_cli.context import AppState, run_command from aai_cli.help_text import examples_epilog diff --git a/aai_cli/webhook_listen.py b/aai_cli/commands/webhooks/_listen.py similarity index 100% rename from aai_cli/webhook_listen.py rename to aai_cli/commands/webhooks/_listen.py diff --git a/tests/_clip_helpers.py b/tests/_clip_helpers.py index 6e05a43a..bda873eb 100644 --- a/tests/_clip_helpers.py +++ b/tests/_clip_helpers.py @@ -15,7 +15,7 @@ import pytest from aai_cli import llm, mediafile -from aai_cli.clip_exec import ClipOptions +from aai_cli.commands.clip._exec import ClipOptions _ANSI_SGR = re.compile(r"\x1b\[[0-9;]*m") diff --git a/tests/_dub_helpers.py b/tests/_dub_helpers.py index e8f314c8..16852e84 100644 --- a/tests/_dub_helpers.py +++ b/tests/_dub_helpers.py @@ -16,7 +16,7 @@ import pytest from aai_cli import client, config, llm, mediafile -from aai_cli.dub_exec import DubOptions +from aai_cli.commands.dub._exec import DubOptions from aai_cli.tts import session from aai_cli.tts.session import SpeakResult diff --git a/tests/test_agent_command.py b/tests/test_agent_command.py index f058ba41..3348c90b 100644 --- a/tests/test_agent_command.py +++ b/tests/test_agent_command.py @@ -33,7 +33,7 @@ def test_list_voices_prints_and_exits_without_connecting(monkeypatch): def fake_run_session(api_key, *, renderer, player, mic, config): called["ran"] = True - monkeypatch.setattr("aai_cli.agent_exec.run_session", fake_run_session) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", fake_run_session) result = runner.invoke(app, ["agent", "--list-voices"]) assert result.exit_code == 0 assert "ivy" in result.output @@ -44,7 +44,7 @@ def fake_run_session(api_key, *, renderer, player, mic, config): def test_list_voices_json_emits_machine_readable_array(monkeypatch): monkeypatch.setattr( - "aai_cli.agent_exec.run_session", + "aai_cli.commands.agent._exec.run_session", lambda *a, **k: (_ for _ in ()).throw(AssertionError("must not connect")), ) result = runner.invoke(app, ["agent", "--list-voices", "--json"]) @@ -59,12 +59,12 @@ def test_list_voices_json_emits_machine_readable_array(monkeypatch): def test_agent_unauthenticated_runs_login(monkeypatch): monkeypatch.setattr("aai_cli.context._interactive_session", lambda: True) monkeypatch.setattr("aai_cli.auth.run_login_flow", _login_result) - monkeypatch.setattr("aai_cli.agent_exec.FileSource", lambda src: f"filesrc:{src}") + monkeypatch.setattr("aai_cli.commands.agent._exec.FileSource", lambda src: f"filesrc:{src}") def fake_run_session(api_key, *, renderer, player, mic, config): raise AssertionError(f"agent session should not run after auto-login: {api_key}") - monkeypatch.setattr("aai_cli.agent_exec.run_session", fake_run_session) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", fake_run_session) result = runner.invoke(app, ["agent", "--sample", "--json"]) assert result.exit_code == 4 assert config.get_api_key("default") == "sk_from_oauth" @@ -79,7 +79,7 @@ def fake_run_session(api_key, *, renderer, player, mic, config): renderer.user_final("hello agent") renderer.agent_transcript("hello human", interrupted=False) - monkeypatch.setattr("aai_cli.agent_exec.run_session", fake_run_session) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", fake_run_session) result = runner.invoke(app, ["agent", "--json"]) assert result.exit_code == 0 lines = [json.loads(x) for x in result.output.splitlines() if x.strip()] @@ -96,7 +96,7 @@ def fake_run_session(api_key, *, renderer, player, mic, config): seen["prompt"] = config.system_prompt seen["full_duplex"] = config.full_duplex - monkeypatch.setattr("aai_cli.agent_exec.run_session", fake_run_session) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", fake_run_session) prompt_file = tmp_path / "p.txt" prompt_file.write_text("be a pirate") result = runner.invoke( @@ -120,7 +120,7 @@ def fake_run_session(api_key, *, renderer, player, mic, config): def test_agent_headphones_notice_in_human_mode(monkeypatch): config.set_api_key("default", "sk_live") monkeypatch.setattr("aai_cli.output.resolve_json", lambda *, explicit: False) - monkeypatch.setattr("aai_cli.agent_exec.run_session", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", lambda *a, **k: None) result = runner.invoke(app, ["agent"]) assert result.exit_code == 0 assert "headphones" in result.output.lower() # mic stays open -> warn to use headphones @@ -132,21 +132,21 @@ def test_agent_ctrl_c_exits_cleanly(monkeypatch): def raise_kbd(*a, **k): raise KeyboardInterrupt - monkeypatch.setattr("aai_cli.agent_exec.run_session", raise_kbd) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", raise_kbd) result = runner.invoke(app, ["agent"]) assert result.exit_code == 0 def test_agent_unknown_voice_exits_2(monkeypatch): config.set_api_key("default", "sk_live") - monkeypatch.setattr("aai_cli.agent_exec.run_session", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", lambda *a, **k: None) result = runner.invoke(app, ["agent", "--voice", "not-a-voice"]) assert result.exit_code == 2 def test_agent_prompt_file_not_found_exits_2(monkeypatch): config.set_api_key("default", "sk_live") - monkeypatch.setattr("aai_cli.agent_exec.run_session", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", lambda *a, **k: None) result = runner.invoke( app, ["agent", "--system-prompt-file", "/tmp/no_such_file_xyz_voiceagent.txt"] ) @@ -160,7 +160,7 @@ def _capture_run_session(monkeypatch): def fake_run_session(api_key, *, renderer, player, mic, config): seen.update(renderer=renderer, player=player, mic=mic, config=config) - monkeypatch.setattr("aai_cli.agent_exec.run_session", fake_run_session) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", fake_run_session) return seen @@ -169,7 +169,7 @@ def test_agent_file_source_streams_clip_and_exits_after_reply(monkeypatch, tmp_p wav = tmp_path / "say.wav" wav.write_bytes(b"RIFF") # FileSource is faked below; contents don't matter - monkeypatch.setattr("aai_cli.agent_exec.FileSource", lambda src: f"filesrc:{src}") + monkeypatch.setattr("aai_cli.commands.agent._exec.FileSource", lambda src: f"filesrc:{src}") seen = _capture_run_session(monkeypatch) result = runner.invoke(app, ["agent", str(wav)]) @@ -192,7 +192,7 @@ def fake_file_source(src): captured["src"] = src return "filesrc" - monkeypatch.setattr("aai_cli.agent_exec.FileSource", fake_file_source) + monkeypatch.setattr("aai_cli.commands.agent._exec.FileSource", fake_file_source) seen = _capture_run_session(monkeypatch) result = runner.invoke(app, ["agent", "--sample"]) @@ -203,7 +203,7 @@ def fake_file_source(src): def test_agent_file_source_with_device_exits_2(monkeypatch, tmp_path): config.set_api_key("default", "sk_live") - monkeypatch.setattr("aai_cli.agent_exec.run_session", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", lambda *a, **k: None) wav = tmp_path / "say.wav" wav.write_bytes(b"RIFF") result = runner.invoke(app, ["agent", str(wav), "--device", "1"]) @@ -213,8 +213,8 @@ def test_agent_file_source_with_device_exits_2(monkeypatch, tmp_path): def test_agent_file_source_no_headphones_notice(monkeypatch, tmp_path): config.set_api_key("default", "sk_live") monkeypatch.setattr("aai_cli.output.resolve_json", lambda *, explicit: False) - monkeypatch.setattr("aai_cli.agent_exec.FileSource", lambda src: "filesrc") - monkeypatch.setattr("aai_cli.agent_exec.run_session", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.agent._exec.FileSource", lambda src: "filesrc") + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", lambda *a, **k: None) wav = tmp_path / "say.wav" wav.write_bytes(b"RIFF") result = runner.invoke(app, ["agent", str(wav)]) @@ -225,12 +225,12 @@ def test_agent_file_source_no_headphones_notice(monkeypatch, tmp_path): def test_agent_file_source_no_start_talking_notice(monkeypatch, tmp_path): config.set_api_key("default", "sk_live") monkeypatch.setattr("aai_cli.output.resolve_json", lambda *, explicit: False) - monkeypatch.setattr("aai_cli.agent_exec.FileSource", lambda src: "filesrc") + monkeypatch.setattr("aai_cli.commands.agent._exec.FileSource", lambda src: "filesrc") def fake_run_session(api_key, *, renderer, player, mic, config): renderer.connected() # session.ready arrives even for a file-driven run - monkeypatch.setattr("aai_cli.agent_exec.run_session", fake_run_session) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", fake_run_session) wav = tmp_path / "say.wav" wav.write_bytes(b"RIFF") result = runner.invoke(app, ["agent", str(wav)]) @@ -255,12 +255,12 @@ def start(self): def close(self): pass - monkeypatch.setattr("aai_cli.agent_exec.DuplexAudio", FakeDuplex) + monkeypatch.setattr("aai_cli.commands.agent._exec.DuplexAudio", FakeDuplex) def fake_run_session(api_key, *, renderer, player, mic, config): renderer.connected() - monkeypatch.setattr("aai_cli.agent_exec.run_session", fake_run_session) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", fake_run_session) result = runner.invoke(app, ["agent"]) assert result.exit_code == 0 assert "start talking" in result.output.lower() # live mic -> prompt the user to speak @@ -269,7 +269,9 @@ def fake_run_session(api_key, *, renderer, player, mic, config): def test_agent_show_code_prints_without_session(monkeypatch): # Print-only: emits the agent script, never starts a session or opens audio, no auth. called = [] - monkeypatch.setattr("aai_cli.agent_exec.run_session", lambda *a, **k: called.append(True)) + monkeypatch.setattr( + "aai_cli.commands.agent._exec.run_session", lambda *a, **k: called.append(True) + ) result = runner.invoke(app, ["agent", "--voice", "ivy", "--show-code"]) assert result.exit_code == 0 assert called == [] # never ran a session @@ -284,7 +286,7 @@ def test_agent_show_code_file_source_warns_on_stderr(monkeypatch): def _boom(*a, **k): raise AssertionError("must not run a session") - monkeypatch.setattr("aai_cli.agent_exec.run_session", _boom) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", _boom) result = _invoke_split(["agent", "clip.wav", "--show-code"]) assert result.exit_code == 0 assert "uses the microphone" in result.stderr @@ -319,7 +321,7 @@ def test_agent_headphones_notice_routes_to_stderr(monkeypatch): # default human mode the notice goes to stderr, stdout stays transcript-only. config.set_api_key("default", "sk_live") monkeypatch.setattr("aai_cli.output.resolve_json", lambda *, explicit: False) - monkeypatch.setattr("aai_cli.agent_exec.run_session", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", lambda *a, **k: None) result = _invoke_split(["agent"]) assert result.exit_code == 0 assert "headphones" in result.stderr.lower() @@ -331,7 +333,7 @@ def _boom(*a, **k): raise AssertionError("must not run a session") monkeypatch.setattr( - "aai_cli.agent_exec.run_session", + "aai_cli.commands.agent._exec.run_session", _boom, ) result = runner.invoke(app, ["agent", "--voice", "ivy", "--show-code", "--json"]) @@ -342,13 +344,13 @@ def _boom(*a, **k): def test_agent_output_text_emits_plain_transcript(monkeypatch): # `-o text` -> plain you:/agent: lines on stdout (pipe into assembly llm). config.set_api_key("default", "sk_live") - monkeypatch.setattr("aai_cli.agent_exec.FileSource", lambda src: "filesrc") + monkeypatch.setattr("aai_cli.commands.agent._exec.FileSource", lambda src: "filesrc") def fake_run_session(api_key, *, renderer, player, mic, config): renderer.user_final("hello there") renderer.agent_transcript("hi, how can I help?", interrupted=False) - monkeypatch.setattr("aai_cli.agent_exec.run_session", fake_run_session) + monkeypatch.setattr("aai_cli.commands.agent._exec.run_session", fake_run_session) result = runner.invoke(app, ["agent", "--sample", "-o", "text"]) assert result.exit_code == 0 assert "you: hello there" in result.output @@ -370,7 +372,7 @@ def test_resolve_system_prompt_unreadable_file_raises_clierror(tmp_path): import pytest - from aai_cli import agent_exec + from aai_cli.commands.agent import _exec as agent_exec from aai_cli.errors import CLIError missing = Path(tmp_path) / "does-not-exist.txt" diff --git a/tests/test_caption_command.py b/tests/test_caption_command.py index 4797cab6..4d394f37 100644 --- a/tests/test_caption_command.py +++ b/tests/test_caption_command.py @@ -10,8 +10,8 @@ from typer.testing import CliRunner -from aai_cli import caption_exec -from aai_cli.caption_exec import CaptionOptions +from aai_cli.commands.caption import _exec as caption_exec +from aai_cli.commands.caption._exec import CaptionOptions from aai_cli.main import app runner = CliRunner() diff --git a/tests/test_caption_exec.py b/tests/test_caption_exec.py index e88954bd..8f4f41c5 100644 --- a/tests/test_caption_exec.py +++ b/tests/test_caption_exec.py @@ -16,8 +16,9 @@ import pytest -from aai_cli import caption_exec, client, config, mediafile, youtube -from aai_cli.caption_exec import CaptionOptions +from aai_cli import client, config, mediafile, youtube +from aai_cli.commands.caption import _exec as caption_exec +from aai_cli.commands.caption._exec import CaptionOptions from aai_cli.context import AppState from aai_cli.errors import CLIError, UsageError from tests._clip_helpers import plain diff --git a/tests/test_clip_command.py b/tests/test_clip_command.py index b7847e40..a6162ee3 100644 --- a/tests/test_clip_command.py +++ b/tests/test_clip_command.py @@ -9,8 +9,9 @@ from typer.testing import CliRunner -from aai_cli import clip_exec, llm, mediafile -from aai_cli.clip_exec import ClipOptions +from aai_cli import llm, mediafile +from aai_cli.commands.clip import _exec as clip_exec +from aai_cli.commands.clip._exec import ClipOptions from aai_cli.main import app runner = CliRunner() diff --git a/tests/test_clip_exec.py b/tests/test_clip_exec.py index efa1ab25..c2a8169e 100644 --- a/tests/test_clip_exec.py +++ b/tests/test_clip_exec.py @@ -16,8 +16,9 @@ import pytest -from aai_cli import client, clip_exec, config, mediafile -from aai_cli.clip_select import Segment +from aai_cli import client, config, mediafile +from aai_cli.commands.clip import _exec as clip_exec +from aai_cli.commands.clip._select import Segment from aai_cli.context import AppState from aai_cli.errors import CLIError, UsageError from tests._clip_helpers import ( diff --git a/tests/test_clip_select.py b/tests/test_clip_select.py index 55e9ffc1..b5022ef2 100644 --- a/tests/test_clip_select.py +++ b/tests/test_clip_select.py @@ -8,8 +8,8 @@ import pytest -from aai_cli import clip_select -from aai_cli.clip_select import Segment +from aai_cli.commands.clip import _select as clip_select +from aai_cli.commands.clip._select import Segment from aai_cli.errors import CLIError, UsageError from tests._clip_helpers import UTTERANCES diff --git a/tests/test_clip_sources.py b/tests/test_clip_sources.py index ab32007c..b4f9a194 100644 --- a/tests/test_clip_sources.py +++ b/tests/test_clip_sources.py @@ -11,7 +11,9 @@ import pytest -from aai_cli import client, clip_exec, clip_select, config +from aai_cli import client, config +from aai_cli.commands.clip import _exec as clip_exec +from aai_cli.commands.clip import _select as clip_select from aai_cli.context import AppState from aai_cli.errors import CLIError, UsageError from tests._clip_helpers import DEFAULTS, UTTERANCES, fake_transcript, record_ffmpeg diff --git a/tests/test_command_options_seam.py b/tests/test_command_options_seam.py index 1ff4ed65..c494a9b1 100644 --- a/tests/test_command_options_seam.py +++ b/tests/test_command_options_seam.py @@ -14,9 +14,12 @@ import pytest import typer -from aai_cli import agent_exec, choices, config, llm, llm_exec, speak_exec, transcribe_exec +from aai_cli import choices, config, llm, transcribe_exec from aai_cli.agent.session import DEFAULT_GREETING, DEFAULT_PROMPT from aai_cli.agent.voices import DEFAULT_VOICE +from aai_cli.commands.agent import _exec as agent_exec +from aai_cli.commands.llm import _exec as llm_exec +from aai_cli.commands.speak import _exec as speak_exec from aai_cli.context import AppState from aai_cli.errors import CLIError, UsageError from aai_cli.options import DEFAULT_BATCH_CONCURRENCY diff --git a/tests/test_deploy.py b/tests/test_deploy.py index 73ba1d82..8c09a112 100644 --- a/tests/test_deploy.py +++ b/tests/test_deploy.py @@ -9,7 +9,7 @@ import pytest from typer.testing import CliRunner -from aai_cli.deploy_exec import FLY, RAILWAY, VERCEL, Target +from aai_cli.commands.deploy._exec import FLY, RAILWAY, VERCEL, Target from aai_cli.main import app runner = CliRunner() @@ -67,7 +67,7 @@ def fake_run(cmd: list[str], *, cwd: Path, check: bool) -> types.SimpleNamespace runs.append({"cmd": cmd, "cwd": cwd, "check": check}) return types.SimpleNamespace(returncode=returncode) - monkeypatch.setattr("aai_cli.deploy_exec.subprocess.run", fake_run) + monkeypatch.setattr("aai_cli.commands.deploy._exec.subprocess.run", fake_run) return calls @@ -358,7 +358,7 @@ def test_deploy_prod_error_suggests_dropping_the_flag(monkeypatch: pytest.Monkey def test_install_hint_platform_selection(monkeypatch: pytest.MonkeyPatch) -> None: - from aai_cli import deploy_exec as deploy + from aai_cli.commands.deploy import _exec as deploy monkeypatch.setattr("sys.platform", "darwin") assert deploy._install_hint(FLY) == "Install it with `brew install flyctl`." diff --git a/tests/test_dictate_command.py b/tests/test_dictate_command.py index 63f12088..8540816d 100644 --- a/tests/test_dictate_command.py +++ b/tests/test_dictate_command.py @@ -3,7 +3,7 @@ from typer.testing import CliRunner -from aai_cli import dictate_exec +from aai_cli.commands.dictate import _exec as dictate_exec from aai_cli.main import app runner = CliRunner() diff --git a/tests/test_dictate_exec.py b/tests/test_dictate_exec.py index f86ca906..b6b89c5f 100644 --- a/tests/test_dictate_exec.py +++ b/tests/test_dictate_exec.py @@ -14,7 +14,8 @@ import pytest -from aai_cli import config, dictate_exec, sync_stt +from aai_cli import config, sync_stt +from aai_cli.commands.dictate import _exec as dictate_exec from aai_cli.context import AppState from aai_cli.errors import CLIError diff --git a/tests/test_dub_command.py b/tests/test_dub_command.py index af64bef2..ecd579b4 100644 --- a/tests/test_dub_command.py +++ b/tests/test_dub_command.py @@ -10,7 +10,8 @@ import pytest from typer.testing import CliRunner -from aai_cli import dub_exec, llm +from aai_cli import llm +from aai_cli.commands.dub import _exec as dub_exec from aai_cli.main import app from tests._clip_helpers import plain diff --git a/tests/test_dub_exec.py b/tests/test_dub_exec.py index 81719a54..dff55a1a 100644 --- a/tests/test_dub_exec.py +++ b/tests/test_dub_exec.py @@ -14,7 +14,8 @@ import pytest -from aai_cli import dub_exec, mediafile +from aai_cli import mediafile +from aai_cli.commands.dub import _exec as dub_exec from aai_cli.context import AppState from aai_cli.errors import CLIError, UsageError from tests._dub_helpers import ( diff --git a/tests/test_dub_pipeline.py b/tests/test_dub_pipeline.py index f4592100..300f1b78 100644 --- a/tests/test_dub_pipeline.py +++ b/tests/test_dub_pipeline.py @@ -16,7 +16,8 @@ import pytest -from aai_cli import client, dub_exec, llm, mediafile +from aai_cli import client, llm, mediafile +from aai_cli.commands.dub import _exec as dub_exec from aai_cli.context import AppState from aai_cli.errors import APIError, CLIError from aai_cli.tts import session diff --git a/tests/test_dub_sources.py b/tests/test_dub_sources.py index 1dac61f0..55bd6d84 100644 --- a/tests/test_dub_sources.py +++ b/tests/test_dub_sources.py @@ -12,7 +12,8 @@ import pytest -from aai_cli import dub_exec, youtube +from aai_cli import youtube +from aai_cli.commands.dub import _exec as dub_exec from aai_cli.context import AppState from aai_cli.errors import UsageError from tests._dub_helpers import ( diff --git a/tests/test_eval_command.py b/tests/test_eval_command.py index 19da9424..1ee69e0f 100644 --- a/tests/test_eval_command.py +++ b/tests/test_eval_command.py @@ -13,7 +13,8 @@ import pytest from typer.testing import CliRunner -from aai_cli import config, eval_data +from aai_cli import config +from aai_cli.commands.evaluate import _data as eval_data from aai_cli.main import app runner = CliRunner() @@ -42,7 +43,7 @@ def _write_wer_manifest(tmp_path): def _mock_transcribe(mocker, results): return mocker.patch( - "aai_cli.evaluate_exec.client.transcribe", + "aai_cli.commands.evaluate._exec.client.transcribe", autospec=True, side_effect=list(results), ) @@ -141,7 +142,7 @@ def _assign(obj, attribute, value): def test_item_results_are_immutable(): - from aai_cli.evaluate_exec import _ItemResult + from aai_cli.commands.evaluate._exec import _ItemResult result = _ItemResult(row={}, words=None) with pytest.raises(dataclasses.FrozenInstanceError): @@ -165,7 +166,9 @@ def _loaded_dataset(): def test_loader_defaults(tmp_path, mocker): _auth() load = mocker.patch( - "aai_cli.evaluate_exec.eval_data.load", autospec=True, return_value=_loaded_dataset() + "aai_cli.commands.evaluate._exec.eval_data.load", + autospec=True, + return_value=_loaded_dataset(), ) _mock_transcribe(mocker, [_transcript("hello")]) result = runner.invoke(app, ["eval", "org/ds"]) @@ -179,7 +182,9 @@ def test_loader_defaults(tmp_path, mocker): def test_explicit_loader_flags_pass_through(tmp_path, mocker): _auth() load = mocker.patch( - "aai_cli.evaluate_exec.eval_data.load", autospec=True, return_value=_loaded_dataset() + "aai_cli.commands.evaluate._exec.eval_data.load", + autospec=True, + return_value=_loaded_dataset(), ) _mock_transcribe(mocker, [_transcript("hello")]) argv = [ @@ -206,7 +211,9 @@ def test_limit_out_of_range_is_a_usage_error(limit): def test_limit_bounds_are_inclusive(tmp_path, mocker, limit): _auth() mocker.patch( - "aai_cli.evaluate_exec.eval_data.load", autospec=True, return_value=_loaded_dataset() + "aai_cli.commands.evaluate._exec.eval_data.load", + autospec=True, + return_value=_loaded_dataset(), ) _mock_transcribe(mocker, [_transcript("hello")]) assert runner.invoke(app, ["eval", "org/ds", "--limit", limit]).exit_code == 0 @@ -223,7 +230,7 @@ def fake_status(message, *, json_mode, quiet): seen.append(message) yield - monkeypatch.setattr("aai_cli.evaluate_exec.output.status", fake_status) + monkeypatch.setattr("aai_cli.commands.evaluate._exec.output.status", fake_status) assert runner.invoke(app, ["eval", "manifest.csv"]).exit_code == 0 assert seen == ["[1/2] Transcribing a.wav…", "[2/2] Transcribing b.wav…"] diff --git a/tests/test_eval_data_hf.py b/tests/test_eval_data_hf.py index 72c42311..c8f4ba4c 100644 --- a/tests/test_eval_data_hf.py +++ b/tests/test_eval_data_hf.py @@ -1,4 +1,4 @@ -"""Hugging Face dataset loading for `assembly eval` (`aai_cli.eval_data`). +"""Hugging Face dataset loading for `assembly eval` (`aai_cli.commands.evaluate._data`). Runs against an httpx MockTransport (the test_auth_ams.py pattern), so pytest-socket stays armed; local-manifest paths live in @@ -10,7 +10,8 @@ import httpx2 as httpx import pytest -from aai_cli import eval_data, eval_hf_api +from aai_cli.commands.evaluate import _data as eval_data +from aai_cli.commands.evaluate import _hf_api as eval_hf_api from aai_cli.errors import APIError, UsageError # ------------------------------------------------------- Hugging Face datasets diff --git a/tests/test_eval_data_manifest.py b/tests/test_eval_data_manifest.py index aa8c5109..7d29525b 100644 --- a/tests/test_eval_data_manifest.py +++ b/tests/test_eval_data_manifest.py @@ -1,4 +1,4 @@ -"""Local-manifest loading for `assembly eval` (`aai_cli.eval_data`). +"""Local-manifest loading for `assembly eval` (`aai_cli.commands.evaluate._data`). Runs against real temp files; the Hugging Face paths live in test_eval_data_hf.py. @@ -9,7 +9,7 @@ import pytest -from aai_cli import eval_data +from aai_cli.commands.evaluate import _data as eval_data from aai_cli.errors import CLIError, UsageError # ---------------------------------------------------------------- local manifests diff --git a/tests/test_eval_failures.py b/tests/test_eval_failures.py index 4c53c09e..61f54881 100644 --- a/tests/test_eval_failures.py +++ b/tests/test_eval_failures.py @@ -13,7 +13,7 @@ import pytest from typer.testing import CliRunner -from aai_cli import evaluate_exec as evaluate +from aai_cli.commands.evaluate import _exec as evaluate from aai_cli.errors import APIError, auth_failure from aai_cli.main import app from tests.test_eval_command import ( @@ -46,7 +46,9 @@ def fake_transcribe(api_key, audio, *, config): return _transcript(texts[Path(audio).name]) mocker.patch( - "aai_cli.evaluate_exec.client.transcribe", autospec=True, side_effect=fake_transcribe + "aai_cli.commands.evaluate._exec.client.transcribe", + autospec=True, + side_effect=fake_transcribe, ) result = runner.invoke(app, ["eval", "manifest.csv", "--concurrency", "2", "--json"]) assert result.exit_code == 0 @@ -67,7 +69,7 @@ def fake_status(message, *, json_mode, quiet): seen.append(message) yield - monkeypatch.setattr("aai_cli.evaluate_exec.output.status", fake_status) + monkeypatch.setattr("aai_cli.commands.evaluate._exec.output.status", fake_status) assert runner.invoke(app, ["eval", "manifest.csv", "--concurrency", "2"]).exit_code == 0 assert seen == ["Transcribing 2 items (concurrency 2)…"] @@ -211,7 +213,7 @@ def test_rejected_key_aborts_eval_with_auth_exit_code(tmp_path, mocker): def test_unauthenticated_fails_before_dataset_download(mocker): # Credentials resolve before the dataset loads: a signed-out user must not # pull the whole dataset first. - load = mocker.patch("aai_cli.evaluate_exec.eval_data.load", autospec=True) + load = mocker.patch("aai_cli.commands.evaluate._exec.eval_data.load", autospec=True) result = runner.invoke(app, ["eval", "org/ds"]) assert result.exit_code == 4 load.assert_not_called() diff --git a/tests/test_llm_command.py b/tests/test_llm_command.py index 46648883..0b1805c0 100644 --- a/tests/test_llm_command.py +++ b/tests/test_llm_command.py @@ -212,7 +212,7 @@ def test_llm_transcript_id_stdin_warning_suppressed_by_quiet(monkeypatch): def test_llm_transcript_id_no_warning_when_stdin_is_a_terminal(monkeypatch): _auth() - monkeypatch.setattr("aai_cli.llm_exec.stdio.stdin_is_piped", lambda: False) + monkeypatch.setattr("aai_cli.commands.llm._exec.stdio.stdin_is_piped", lambda: False) monkeypatch.setattr("aai_cli.commands.llm.gateway.complete", lambda *a, **k: _payload("s")) result = runner.invoke(app, ["llm", "summarize", "--transcript-id", "t_9"]) assert result.exit_code == 0 @@ -388,7 +388,7 @@ def test_llm_follow_requires_a_prompt(monkeypatch): def test_llm_follow_requires_piped_stdin(monkeypatch): # Interactively (no pipe) --follow would block forever; reject it with guidance. _auth() - monkeypatch.setattr("aai_cli.llm_exec.stdio.stdin_is_piped", lambda: False) + monkeypatch.setattr("aai_cli.commands.llm._exec.stdio.stdin_is_piped", lambda: False) monkeypatch.setattr("aai_cli.commands.llm.gateway.complete", lambda *a, **k: _payload()) result = runner.invoke(app, ["llm", "summarize", "--follow", "--json"]) assert result.exit_code == 2 @@ -424,7 +424,9 @@ def __iter__(self): def __next__(self): raise KeyboardInterrupt - monkeypatch.setattr("aai_cli.llm_exec.stdio.iter_piped_stdin_lines", lambda: _InterruptIter()) + monkeypatch.setattr( + "aai_cli.commands.llm._exec.stdio.iter_piped_stdin_lines", lambda: _InterruptIter() + ) monkeypatch.setattr("aai_cli.commands.llm.gateway.complete", lambda *a, **k: _payload()) result = runner.invoke(app, ["llm", "summarize", "--follow", "--json"], input="") assert result.exit_code == 0 diff --git a/tests/test_llm_config_overrides.py b/tests/test_llm_config_overrides.py index a7c7fd8a..e303b786 100644 --- a/tests/test_llm_config_overrides.py +++ b/tests/test_llm_config_overrides.py @@ -117,7 +117,7 @@ def fake_complete(api_key, *, model, messages, max_tokens, transcript_id=None, e seen.append(extra) return _response("ans") - monkeypatch.setattr("aai_cli.llm_exec.gateway.complete", fake_complete) + monkeypatch.setattr("aai_cli.commands.llm._exec.gateway.complete", fake_complete) result = runner.invoke( app, ["llm", "-f", "summarize", "--config", "temperature=0.1", "--json"], diff --git a/tests/test_source_validation.py b/tests/test_source_validation.py index 2ba88c3a..6742a6ab 100644 --- a/tests/test_source_validation.py +++ b/tests/test_source_validation.py @@ -168,7 +168,7 @@ def test_transcribe_empty_stdin_exits_2(): def test_stream_missing_file_fails_before_credentials(monkeypatch): called = {"stream": False} monkeypatch.setattr( - "aai_cli.stream_exec.client.stream_audio", + "aai_cli.commands.stream._exec.client.stream_audio", lambda *a, **k: called.__setitem__("stream", True), ) result = runner.invoke(app, ["stream", "missing.wav"]) diff --git a/tests/test_speak.py b/tests/test_speak.py index 91a0d782..4392cf57 100644 --- a/tests/test_speak.py +++ b/tests/test_speak.py @@ -47,7 +47,7 @@ def test_production_env_is_rejected_with_sandbox_hint(): def test_plays_audio_by_default(monkeypatch, fake_synthesize): played: dict = {} monkeypatch.setattr( - "aai_cli.speak_exec.audio.play_pcm", + "aai_cli.commands.speak._exec.audio.play_pcm", lambda pcm, rate, **_: played.update(pcm=pcm, rate=rate), ) result = runner.invoke(app, ["--sandbox", "speak", "Hello there"]) @@ -63,12 +63,12 @@ def test_plays_audio_by_default(monkeypatch, fake_synthesize): def test_out_writes_wav_and_does_not_play(monkeypatch, tmp_path, fake_synthesize): monkeypatch.setattr( - "aai_cli.speak_exec.audio.play_pcm", + "aai_cli.commands.speak._exec.audio.play_pcm", lambda *a, **k: pytest.fail("should not play when --out is given"), ) written: dict = {} monkeypatch.setattr( - "aai_cli.speak_exec.audio.write_wav", + "aai_cli.commands.speak._exec.audio.write_wav", lambda path, pcm, rate: written.update(path=path, pcm=pcm, rate=rate), ) out = tmp_path / "x.wav" @@ -82,7 +82,7 @@ def test_out_writes_wav_and_does_not_play(monkeypatch, tmp_path, fake_synthesize def test_reads_text_from_stdin_when_arg_omitted(monkeypatch, fake_synthesize): - monkeypatch.setattr("aai_cli.speak_exec.audio.play_pcm", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.speak._exec.audio.play_pcm", lambda *a, **k: None) result = runner.invoke(app, ["--sandbox", "speak"], input="piped text\n") assert result.exit_code == 0 assert fake_synthesize["cfg"].text == "piped text" @@ -104,7 +104,7 @@ def test_blank_arg_does_not_fall_back_to_stdin(monkeypatch): def test_voice_and_language_flow_into_config(monkeypatch, fake_synthesize): - monkeypatch.setattr("aai_cli.speak_exec.audio.play_pcm", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.speak._exec.audio.play_pcm", lambda *a, **k: None) result = runner.invoke( app, ["--sandbox", "speak", "Hi", "--voice", "jane", "--language", "English"] ) @@ -118,7 +118,7 @@ def test_voice_and_language_flow_into_config(monkeypatch, fake_synthesize): def test_default_voice_follows_the_language(monkeypatch, fake_synthesize): # Each voice speaks one language: with no --voice, a non-English --language # switches to that language's native voice instead of English "jane". - monkeypatch.setattr("aai_cli.speak_exec.audio.play_pcm", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.speak._exec.audio.play_pcm", lambda *a, **k: None) result = runner.invoke(app, ["--sandbox", "speak", "Ciao", "--language", "Italian"]) assert result.exit_code == 0 cfg = fake_synthesize["cfg"] @@ -127,7 +127,7 @@ def test_default_voice_follows_the_language(monkeypatch, fake_synthesize): def test_explicit_voice_beats_the_language_default(monkeypatch, fake_synthesize): - monkeypatch.setattr("aai_cli.speak_exec.audio.play_pcm", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.speak._exec.audio.play_pcm", lambda *a, **k: None) result = runner.invoke( app, ["--sandbox", "speak", "Bonjour", "--voice", "jane", "--language", "French"] ) @@ -137,7 +137,7 @@ def test_explicit_voice_beats_the_language_default(monkeypatch, fake_synthesize) def test_json_mode_emits_metadata_object_on_stdout(monkeypatch, fake_synthesize): - monkeypatch.setattr("aai_cli.speak_exec.audio.play_pcm", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.speak._exec.audio.play_pcm", lambda *a, **k: None) result = runner.invoke(app, ["--sandbox", "speak", "Hi", "--voice", "jane", "--json"]) assert result.exit_code == 0 # The behavioral split: --json yields a parseable object, not human prose. @@ -152,7 +152,7 @@ def test_json_mode_emits_metadata_object_on_stdout(monkeypatch, fake_synthesize) def test_human_mode_keeps_stdout_clean(monkeypatch, fake_synthesize): - monkeypatch.setattr("aai_cli.speak_exec.audio.play_pcm", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.speak._exec.audio.play_pcm", lambda *a, **k: None) result = runner.invoke(app, ["--sandbox", "speak", "Hi"]) assert result.exit_code == 0 # Human summary goes to stderr; stdout stays empty (audio went to the speaker). @@ -171,7 +171,7 @@ def _fake(api_key, segments, *, language=None, sample_rate=None, connect=None, o ) monkeypatch.setattr(session, "synthesize_dialogue", _fake) - monkeypatch.setattr("aai_cli.speak_exec.audio.play_pcm", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.speak._exec.audio.play_pcm", lambda *a, **k: None) return calls @@ -234,7 +234,7 @@ def test_dialogue_json_reports_speaker_voice_map(fake_dialogue): def test_dialogue_json_out_path_is_reported(fake_dialogue, monkeypatch, tmp_path): # With --out, the multi JSON reports the file path (not null) — pins the # `str(out) if out is not None else None` branch in _emit_multi. - monkeypatch.setattr("aai_cli.speak_exec.audio.write_wav", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.speak._exec.audio.write_wav", lambda *a, **k: None) out = tmp_path / "dialogue.wav" text = "Speaker A: One.\nSpeaker B: Two." result = runner.invoke(app, ["--sandbox", "speak", "--out", str(out), "--json"], input=text) @@ -254,7 +254,7 @@ def test_empty_speaker_labels_raises_usage_error(): def test_unlabeled_text_still_uses_single_voice_path(fake_synthesize, monkeypatch): # A bare --voice still selects the single-voice voice for ordinary prose. - monkeypatch.setattr("aai_cli.speak_exec.audio.play_pcm", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.speak._exec.audio.play_pcm", lambda *a, **k: None) result = runner.invoke(app, ["--sandbox", "speak", "Just prose.", "--voice", "mary"]) assert result.exit_code == 0 assert fake_synthesize["cfg"].voice == "mary" @@ -266,7 +266,7 @@ def test_unlabeled_text_still_uses_single_voice_path(fake_synthesize, monkeypatc def test_speaker_mappings_on_unlabeled_input_warn_not_silently_drop(fake_synthesize, monkeypatch): # The mirror of the bare-voice-in-dialogue note: SPEAKER=VOICE mappings can't # apply to plain prose, and the user is told instead of the flag vanishing. - monkeypatch.setattr("aai_cli.speak_exec.audio.play_pcm", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.speak._exec.audio.play_pcm", lambda *a, **k: None) result = runner.invoke(app, ["--sandbox", "speak", "Just prose.", "--voice", "A=vera"]) assert result.exit_code == 0 assert "Ignoring --voice SPEAKER=VOICE mappings" in result.stderr @@ -276,7 +276,7 @@ def test_speaker_mappings_on_unlabeled_input_warn_not_silently_drop(fake_synthes def test_speaker_mappings_warning_is_structured_in_json_mode(fake_synthesize, monkeypatch): - monkeypatch.setattr("aai_cli.speak_exec.audio.play_pcm", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.speak._exec.audio.play_pcm", lambda *a, **k: None) result = runner.invoke( app, ["--sandbox", "speak", "Just prose.", "--voice", "A=vera", "--json"] ) @@ -298,7 +298,7 @@ def test_sample_rate_must_be_positive(): def test_sample_rate_floor_accepts_one(fake_synthesize, monkeypatch): # min=1 exactly: 1 Hz is degenerate but valid (the server enforces its own floor). - monkeypatch.setattr("aai_cli.speak_exec.audio.play_pcm", lambda *a, **k: None) + monkeypatch.setattr("aai_cli.commands.speak._exec.audio.play_pcm", lambda *a, **k: None) result = runner.invoke(app, ["--sandbox", "speak", "Hi", "--sample-rate", "1"]) assert result.exit_code == 0 assert fake_synthesize["cfg"].sample_rate == 1 diff --git a/tests/test_stream_command.py b/tests/test_stream_command.py index 2d079865..8676555c 100644 --- a/tests/test_stream_command.py +++ b/tests/test_stream_command.py @@ -43,7 +43,7 @@ def test_stream_help_lists_command(): def test_stream_mic_renders_turns(monkeypatch): config.set_api_key("default", "sk_live") - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", _drive_turns) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", _drive_turns) result = runner.invoke(app, ["stream", "--json"]) assert result.exit_code == 0 lines = [json.loads(x) for x in result.output.splitlines() if x.strip()] @@ -60,7 +60,7 @@ def fake_stream_audio( seen["source_type"] = type(source).__name__ seen["rate"] = params.sample_rate - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream_audio) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream_audio) import wave p = tmp_path / "a.wav" @@ -90,7 +90,7 @@ def __iter__(self): captured["on_open"]() # the SDK iterating us == the mic is now live return iter([b"\x00\x00"]) - monkeypatch.setattr("aai_cli.stream_exec.MicrophoneSource", FakeMic) + monkeypatch.setattr("aai_cli.commands.stream._exec.MicrophoneSource", FakeMic) order = [] @@ -103,7 +103,7 @@ def fake_stream_audio( list(source) # consume the mic -> on_open fires -> "Listening…" prints order.append("consumed") - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream_audio) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream_audio) result = runner.invoke(app, ["stream"]) assert result.exit_code == 0 assert "Listening" in result.output # shown once the mic opened @@ -118,7 +118,7 @@ def fake(api_key, source, *, params, on_begin=None, on_turn=None, on_termination if on_begin: on_begin(types.SimpleNamespace(id="x")) - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake) import wave p = tmp_path / "a.wav" @@ -141,7 +141,7 @@ def fake_stream_audio( ): raise AssertionError(f"streaming should not start after auto-login: {api_key}") - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream_audio) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream_audio) result = runner.invoke(app, ["stream", "--json"]) assert result.exit_code == 4 assert config.get_api_key("default") == "sk_from_oauth" @@ -162,7 +162,7 @@ def test_stream_sample_uses_hosted_clip(monkeypatch): config.set_api_key("default", "sk_live") monkeypatch.setattr("aai_cli.streaming.sources.shutil.which", lambda _n: "/usr/bin/ffmpeg") seen = {} - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", _capture_source(seen)) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", _capture_source(seen)) result = runner.invoke(app, ["stream", "--sample"]) assert result.exit_code == 0 assert type(seen["source"]).__name__ == "FileSource" @@ -174,7 +174,7 @@ def test_stream_url_source_uses_filesource(monkeypatch): config.set_api_key("default", "sk_live") monkeypatch.setattr("aai_cli.streaming.sources.shutil.which", lambda _n: "/usr/bin/ffmpeg") seen = {} - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", _capture_source(seen)) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", _capture_source(seen)) result = runner.invoke(app, ["stream", "https://example.com/clip.mp3"]) assert result.exit_code == 0 assert type(seen["source"]).__name__ == "FileSource" @@ -187,7 +187,7 @@ def test_stream_ctrl_c_exits_cleanly(monkeypatch): def raise_kbd(*a, **k): raise KeyboardInterrupt - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", raise_kbd) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", raise_kbd) result = runner.invoke(app, ["stream"]) assert result.exit_code == 0 @@ -199,7 +199,7 @@ def test_stream_ctrl_c_human_mode_prints_stopped(monkeypatch): def raise_kbd(*a, **k): raise KeyboardInterrupt - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", raise_kbd) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", raise_kbd) result = runner.invoke(app, ["stream"]) assert result.exit_code == 0 assert "Stopped." in result.output @@ -211,7 +211,7 @@ def test_stream_broken_pipe_exits_zero(monkeypatch): def raise_broken_pipe(*a, **k): raise BrokenPipeError - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", raise_broken_pipe) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", raise_broken_pipe) result = runner.invoke(app, ["stream"]) assert result.exit_code == 0 @@ -232,7 +232,7 @@ def fake(api_key, source, *, params, on_begin=None, on_turn=None, on_termination if on_termination: on_termination(types.SimpleNamespace(audio_duration_seconds=2.0)) - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake) p = tmp_path / "a.wav" with wave.open(str(p), "wb") as w: w.setnchannels(1) @@ -254,7 +254,7 @@ def test_stream_prompt_biases_speech_model(monkeypatch): def fake(api_key, source, *, params, on_begin=None, on_turn=None, on_termination=None): seen["prompt"] = params.prompt - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake) result = runner.invoke(app, ["stream", "--prompt", "expect crypto jargon", "--json"]) assert result.exit_code == 0 # --prompt is the speech-model prompt, forwarded to the streaming session. @@ -271,14 +271,14 @@ def test_stream_youtube_url_downloads_then_streams(monkeypatch, tmp_path): w.setsampwidth(2) w.setframerate(16000) w.writeframes(b"\x00\x01" * 100) - monkeypatch.setattr("aai_cli.stream_exec.youtube.download_media", lambda url, d: fake) + monkeypatch.setattr("aai_cli.commands.stream._exec.youtube.download_media", lambda url, d: fake) seen = {} def fake_stream(api_key, source, *, params, on_begin=None, on_turn=None, on_termination=None): seen["source_type"] = type(source).__name__ seen["src"] = getattr(source, "source", None) - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream) result = runner.invoke(app, ["stream", "https://youtu.be/abc"]) assert result.exit_code == 0 assert seen["source_type"] == "FileSource" # streamed the downloaded local file @@ -295,14 +295,14 @@ def test_stream_podcast_page_url_downloads_then_streams(monkeypatch, tmp_path): w.setsampwidth(2) w.setframerate(16000) w.writeframes(b"\x00\x01" * 100) - monkeypatch.setattr("aai_cli.stream_exec.youtube.download_media", lambda url, d: fake) + monkeypatch.setattr("aai_cli.commands.stream._exec.youtube.download_media", lambda url, d: fake) seen = {} def fake_stream(api_key, source, *, params, on_begin=None, on_turn=None, on_termination=None): seen["source_type"] = type(source).__name__ seen["src"] = getattr(source, "source", None) - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream) result = runner.invoke(app, ["stream", "https://www.spreaker.com/episode/12345"]) assert result.exit_code == 0 assert seen["source_type"] == "FileSource" # streamed the downloaded local file @@ -317,11 +317,11 @@ def test_stream_downloadable_url_resolves_credentials_before_downloading(monkeyp monkeypatch.setattr("aai_cli.context._interactive_session", lambda: False) downloads = [] monkeypatch.setattr( - "aai_cli.stream_exec.youtube.download_media", + "aai_cli.commands.stream._exec.youtube.download_media", lambda url, dest: downloads.append(url), ) monkeypatch.setattr( - "aai_cli.stream_exec.client.stream_audio", + "aai_cli.commands.stream._exec.client.stream_audio", lambda *a, **k: pytest.fail("must not stream without credentials"), ) result = runner.invoke(app, ["stream", "https://youtu.be/abc"]) @@ -351,7 +351,7 @@ def fake_stream_audio( seen["rate"] = params.sample_rate b"".join(source) # drain the StdinSource - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream_audio) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream_audio) result = runner.invoke(app, ["stream", "-", "--sample-rate", "1"], input=b"\x00\x00") assert result.exit_code == 0 assert seen["rate"] == 1 @@ -367,7 +367,7 @@ def fake_stream_audio( seen["rate"] = params.sample_rate seen["audio"] = b"".join(source) # consume the StdinSource - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream_audio) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream_audio) result = runner.invoke(app, ["stream", "-"], input=b"\x01\x02" * 100) assert result.exit_code == 0 assert seen["rate"] == 16000 # default raw-PCM rate @@ -398,9 +398,9 @@ def fake_stream_audio( raise APIError("mic failed") time.sleep(0.2) - monkeypatch.setattr("aai_cli.stream_exec.MacSystemAudioSource", FakeSystemAudio) - monkeypatch.setattr("aai_cli.stream_exec.MicrophoneSource", FakeMic) - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream_audio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MacSystemAudioSource", FakeSystemAudio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MicrophoneSource", FakeMic) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream_audio) result = runner.invoke(app, ["stream", "--system-audio", "--json"]) assert result.exit_code == 1 assert "mic failed" in result.output @@ -417,7 +417,7 @@ def fake_stream_audio( on_turn(types.SimpleNamespace(transcript="partial", end_of_turn=False)) on_turn(types.SimpleNamespace(transcript="hello world", end_of_turn=True)) - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream_audio) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream_audio) result = runner.invoke(app, ["stream", "-", "-o", "text"], input=b"\x00\x00") assert result.exit_code == 0 # Final turn only, plain text; partials and JSON envelopes are not on stdout. diff --git a/tests/test_stream_command_flags.py b/tests/test_stream_command_flags.py index d8144589..af087cdb 100644 --- a/tests/test_stream_command_flags.py +++ b/tests/test_stream_command_flags.py @@ -20,7 +20,7 @@ def fake_stream_audio( ): captured["params"] = params - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream_audio) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream_audio) runner.invoke( app, @@ -43,7 +43,7 @@ def test_stream_config_escape_hatch(monkeypatch): config.set_api_key("default", "sk_live") captured = {} monkeypatch.setattr( - "aai_cli.stream_exec.client.stream_audio", + "aai_cli.commands.stream._exec.client.stream_audio", lambda api_key, source, *, params, **kw: captured.update(params=params), ) @@ -55,7 +55,7 @@ def test_stream_maps_webhook_auth_header(monkeypatch): config.set_api_key("default", "sk_live") captured = {} monkeypatch.setattr( - "aai_cli.stream_exec.client.stream_audio", + "aai_cli.commands.stream._exec.client.stream_audio", lambda api_key, source, *, params, **kw: captured.update(params=params), ) @@ -79,7 +79,7 @@ def test_stream_format_turns_tristate(monkeypatch): config.set_api_key("default", "sk_live") captured = {} monkeypatch.setattr( - "aai_cli.stream_exec.client.stream_audio", + "aai_cli.commands.stream._exec.client.stream_audio", lambda api_key, source, *, params, **kw: captured.update(params=params), ) @@ -134,7 +134,7 @@ def test_stream_file_source_with_sample_rejected(monkeypatch, tmp_path): def _boom(*a, **k): raise AssertionError("must not stream a conflicting source") - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", _boom) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", _boom) wav = tmp_path / "a.wav" wav.write_bytes(b"RIFF") result = runner.invoke(app, ["stream", str(wav), "--sample"]) diff --git a/tests/test_stream_exec.py b/tests/test_stream_exec.py index 2e945522..1e1d9b2a 100644 --- a/tests/test_stream_exec.py +++ b/tests/test_stream_exec.py @@ -1,4 +1,4 @@ -"""Direct tests of the `assembly stream` options/run seam (aai_cli.stream_exec). +"""Direct tests of the `assembly stream` options/run seam (aai_cli.commands.stream._exec). The command module only parses argv into a StreamOptions; everything after that is run_stream, a plain function of data. These tests drive validation, flag mapping, @@ -12,8 +12,9 @@ import pytest -from aai_cli import config, llm, stream_exec +from aai_cli import config, llm from aai_cli.commands.stream import DEFAULT_SPEECH_MODEL +from aai_cli.commands.stream import _exec as stream_exec from aai_cli.context import AppState from aai_cli.errors import UsageError diff --git a/tests/test_stream_llm.py b/tests/test_stream_llm.py index 2825f4b0..7e935d57 100644 --- a/tests/test_stream_llm.py +++ b/tests/test_stream_llm.py @@ -27,7 +27,7 @@ def fake_run_chain(api_key, prompts, *, transcript_text, model, max_tokens): seen["max_tokens"] = max_tokens return f"answer:{transcript_text}" - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake) monkeypatch.setattr("aai_cli.llm.run_chain", fake_run_chain) result = runner.invoke( app, @@ -67,7 +67,7 @@ def fake_run_chain(api_key, prompts, *, transcript_text, model, max_tokens): seen["prompts"] = prompts return "done" - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake) monkeypatch.setattr("aai_cli.llm.run_chain", fake_run_chain) result = runner.invoke( app, ["stream", "--llm", "summarize", "--llm", "translate to french", "--json"] @@ -79,7 +79,7 @@ def fake_run_chain(api_key, prompts, *, transcript_text, model, max_tokens): def test_stream_llm_rejects_output_text(monkeypatch): config.set_api_key("default", "sk_live") monkeypatch.setattr( - "aai_cli.stream_exec.client.stream_audio", + "aai_cli.commands.stream._exec.client.stream_audio", lambda *a, **k: (_ for _ in ()).throw(AssertionError("must not stream")), ) result = runner.invoke(app, ["stream", "--llm", "summarize", "-o", "text"]) @@ -100,7 +100,7 @@ def fake_run_chain(api_key, prompts, *, transcript_text, model, max_tokens): called["ran"] = True return "x" - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake) monkeypatch.setattr("aai_cli.llm.run_chain", fake_run_chain) result = runner.invoke(app, ["stream", "--json"]) assert result.exit_code == 0 @@ -111,7 +111,7 @@ def test_stream_show_code_with_llm_emits_follow_loop(monkeypatch): def _boom(*a, **k): raise AssertionError("must not stream") - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", _boom) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", _boom) result = runner.invoke(app, ["stream", "--llm", "summarize", "--show-code"]) assert result.exit_code == 0 assert "from openai import OpenAI" in result.output diff --git a/tests/test_stream_session.py b/tests/test_stream_session.py index 73226d84..6ce4a4d9 100644 --- a/tests/test_stream_session.py +++ b/tests/test_stream_session.py @@ -64,7 +64,7 @@ def test_stream_session_closes_renderer_on_error(monkeypatch): def boom(*_args, **_kwargs): raise CLIError("stream blew up") - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", boom) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", boom) session = StreamSession( api_key="sk", base_flags={}, @@ -122,9 +122,9 @@ def fake_stream_audio( if on_turn: on_turn(types.SimpleNamespace(transcript=source_type, end_of_turn=True)) - monkeypatch.setattr("aai_cli.stream_exec.MacSystemAudioSource", FakeSystemAudio) - monkeypatch.setattr("aai_cli.stream_exec.MicrophoneSource", FakeMic) - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream_audio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MacSystemAudioSource", FakeSystemAudio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MicrophoneSource", FakeMic) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream_audio) result = runner.invoke(app, ["stream", "--system-audio", "--json"]) assert result.exit_code == 0 assert set(source_types) == {"FakeSystemAudio", "FakeMic"} @@ -154,9 +154,9 @@ def __iter__(self): def fail_mic(**_kwargs): raise AssertionError("system-audio-only must not open the microphone") - monkeypatch.setattr("aai_cli.stream_exec.MacSystemAudioSource", FakeSystemAudio) - monkeypatch.setattr("aai_cli.stream_exec.MicrophoneSource", fail_mic) - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", _capture_source(seen)) + monkeypatch.setattr("aai_cli.commands.stream._exec.MacSystemAudioSource", FakeSystemAudio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MicrophoneSource", fail_mic) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", _capture_source(seen)) result = runner.invoke(app, ["stream", "--system-audio-only", "--json"]) assert result.exit_code == 0 assert type(seen["source"]).__name__ == "FakeSystemAudio" @@ -195,9 +195,9 @@ def fake_stream_audio( ): list(source) - monkeypatch.setattr("aai_cli.stream_exec.MacSystemAudioSource", FakeSystemAudio) - monkeypatch.setattr("aai_cli.stream_exec.MicrophoneSource", FakeMic) - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream_audio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MacSystemAudioSource", FakeSystemAudio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MicrophoneSource", FakeMic) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream_audio) result = runner.invoke( app, ["stream", "--system-audio", "--device", "2", "--sample-rate", "44100", "--json"], @@ -235,9 +235,9 @@ def fake_run_chain(api_key, prompts, *, transcript_text, model, max_tokens): transcript_inputs.append(transcript_text) return "summary" - monkeypatch.setattr("aai_cli.stream_exec.MacSystemAudioSource", FakeSystemAudio) - monkeypatch.setattr("aai_cli.stream_exec.MicrophoneSource", FakeMic) - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream_audio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MacSystemAudioSource", FakeSystemAudio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MicrophoneSource", FakeMic) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream_audio) monkeypatch.setattr("aai_cli.llm.run_chain", fake_run_chain) result = runner.invoke(app, ["stream", "--system-audio", "--llm", "summarize", "--json"]) assert result.exit_code == 0 @@ -271,9 +271,9 @@ def fake_stream_audio( chunk = next(iter(source)) speaker_labels_by_chunk[chunk] = params.speaker_labels - monkeypatch.setattr("aai_cli.stream_exec.MacSystemAudioSource", FakeSystemAudio) - monkeypatch.setattr("aai_cli.stream_exec.MicrophoneSource", FakeMic) - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream_audio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MacSystemAudioSource", FakeSystemAudio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MicrophoneSource", FakeMic) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream_audio) result = runner.invoke(app, ["stream", "--system-audio", "--speaker-labels", "--json"]) assert result.exit_code == 0 assert speaker_labels_by_chunk[b"system"] is True @@ -319,9 +319,9 @@ def fake_stream_audio( ): raise APIError(f"{type(source).__name__} failed") - monkeypatch.setattr("aai_cli.stream_exec.MacSystemAudioSource", FakeSystemAudio) - monkeypatch.setattr("aai_cli.stream_exec.MicrophoneSource", FakeMic) - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream_audio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MacSystemAudioSource", FakeSystemAudio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MicrophoneSource", FakeMic) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream_audio) monkeypatch.setattr("aai_cli.streaming.session.threading.Thread", ImmediateThread) result = runner.invoke(app, ["stream", "--system-audio", "--json"]) assert result.exit_code == 1 @@ -367,9 +367,9 @@ def join(self, timeout=None): def fake_stream_audio(api_key, source, *, params, **_kwargs): raise RuntimeError("event parsing blew up") - monkeypatch.setattr("aai_cli.stream_exec.MacSystemAudioSource", FakeSystemAudio) - monkeypatch.setattr("aai_cli.stream_exec.MicrophoneSource", FakeMic) - monkeypatch.setattr("aai_cli.stream_exec.client.stream_audio", fake_stream_audio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MacSystemAudioSource", FakeSystemAudio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MicrophoneSource", FakeMic) + monkeypatch.setattr("aai_cli.commands.stream._exec.client.stream_audio", fake_stream_audio) monkeypatch.setattr("aai_cli.streaming.session.threading.Thread", ImmediateThread) result = runner.invoke(app, ["stream", "--system-audio", "--json"]) assert result.exit_code == 1 @@ -398,8 +398,8 @@ def __init__(self, *, target, args, daemon): def start(self): raise KeyboardInterrupt - monkeypatch.setattr("aai_cli.stream_exec.MacSystemAudioSource", FakeSystemAudio) - monkeypatch.setattr("aai_cli.stream_exec.MicrophoneSource", FakeMic) + monkeypatch.setattr("aai_cli.commands.stream._exec.MacSystemAudioSource", FakeSystemAudio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MicrophoneSource", FakeMic) monkeypatch.setattr("aai_cli.streaming.session.threading.Thread", InterruptingThread) result = runner.invoke(app, ["stream", "--system-audio"]) assert result.exit_code == 0 @@ -424,8 +424,8 @@ def __init__(self, *, target, args, daemon): def start(self): raise BrokenPipeError - monkeypatch.setattr("aai_cli.stream_exec.MacSystemAudioSource", FakeSystemAudio) - monkeypatch.setattr("aai_cli.stream_exec.MicrophoneSource", FakeMic) + monkeypatch.setattr("aai_cli.commands.stream._exec.MacSystemAudioSource", FakeSystemAudio) + monkeypatch.setattr("aai_cli.commands.stream._exec.MicrophoneSource", FakeMic) monkeypatch.setattr("aai_cli.streaming.session.threading.Thread", BrokenPipeThread) result = runner.invoke(app, ["stream", "--system-audio"]) assert result.exit_code == 0 diff --git a/tests/test_stream_show_code.py b/tests/test_stream_show_code.py index 79e1589d..8246b575 100644 --- a/tests/test_stream_show_code.py +++ b/tests/test_stream_show_code.py @@ -15,7 +15,7 @@ def test_stream_show_code_prints_without_streaming(monkeypatch): # Print-only: emits the mic-streaming script, never opens audio or streams, no auth. called = [] monkeypatch.setattr( - "aai_cli.stream_exec.client.stream_audio", + "aai_cli.commands.stream._exec.client.stream_audio", lambda *a, **k: called.append(True), ) result = runner.invoke(app, ["stream", "--show-code"]) @@ -101,7 +101,7 @@ def _boom(*a, **k): raise AssertionError("must not stream") monkeypatch.setattr( - "aai_cli.stream_exec.client.stream_audio", + "aai_cli.commands.stream._exec.client.stream_audio", _boom, ) result = runner.invoke(app, ["stream", "--show-code", "--json"]) diff --git a/tests/test_webhook_listen.py b/tests/test_webhook_listen.py index 41843264..b3395478 100644 --- a/tests/test_webhook_listen.py +++ b/tests/test_webhook_listen.py @@ -14,7 +14,7 @@ import pytest from typer.testing import CliRunner -from aai_cli import webhook_listen +from aai_cli.commands.webhooks import _listen as webhook_listen from aai_cli.main import app runner = CliRunner() @@ -306,7 +306,7 @@ def fake_find_free_port(preferred, **kwargs): ) # Stop immediately: the listener loop isn't under test here. monkeypatch.setattr( - "aai_cli.webhook_listen.ThreadingHTTPServer.serve_forever", _raise_interrupt + "aai_cli.commands.webhooks._listen.ThreadingHTTPServer.serve_forever", _raise_interrupt ) return proc, log, seen, real_port @@ -342,7 +342,7 @@ def test_listen_tunnel_url_timeout_errors_and_keeps_the_log(tmp_path, monkeypatc def test_listen_accepts_explicit_max_events_zero(monkeypatch): # 0 is the documented "until Ctrl-C" value; the option's floor must not reject it. monkeypatch.setattr( - "aai_cli.webhook_listen.ThreadingHTTPServer.serve_forever", _raise_interrupt + "aai_cli.commands.webhooks._listen.ThreadingHTTPServer.serve_forever", _raise_interrupt ) result = runner.invoke(app, ["webhooks", "listen", "--no-tunnel", "--max-events", "0"]) assert result.exit_code == 0, result.output