diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff5ab32..834d3f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,7 @@ jobs: run: | $ErrorActionPreference = 'Stop' $files = @( - 'src/plaud_tools/scripts/update.ps1', + 'src/plaud_tools/tray/scripts/update.ps1', 'scripts/install.ps1' ) $failed = $false @@ -210,7 +210,7 @@ jobs: # Wave 2 / C4 — frozen psutil import proof. # # Build the tray bundle and run PlaudTools.exe --diagnose-enum. This flag - # imports plaud_tools.mcp_lifecycle (exercising all hiddenimports) and + # imports plaud_tools.cli.process_probe (exercising all hiddenimports) and # prints "enumerator=psutil" when psutil is bundled correctly, then exits 0 # WITHOUT starting the tkinter/pystray GUI (critical: no display on CI). # diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d0589..6bb05b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **Internal: reorganized `src/plaud_tools/` by surface instead of a flat file list.** + New packages `core/` (shared client/session/auth/transport/etc., used by all + three surfaces), `cli/`, `mcp_pt/` (named to avoid shadowing the `mcp` SDK + package), and consolidated `assets/completions/scripts` PS1-template data + into `tray/` where they're actually consumed. `mcp_lifecycle.py` renamed to + `cli/process_probe.py` (its name had outlived a prior dead-code removal). + No behavior, CLI/MCP surface, or entry-point change — pure module reorg for + maintainability. + ## [0.7.1] - 2026-07-07 Follow-up patch that formally closes the last open items from the #138–#164 diff --git a/CLAUDE.md b/CLAUDE.md index 86edc6f..08776df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,12 +4,12 @@ This file provides working guidance for agents operating in this repository. ## Current State -The Python rewrite is complete. The active code lives under `src/plaud_tools/`: +The Python rewrite is complete. The active code lives under `src/plaud_tools/`, grouped by which surface owns each concern: -- `client.py` — Plaud domain/client layer (auth, session, all API flows) -- `cli.py` — Python CLI (`plaud-tools` / `pt` entry points) -- `mcp.py` — MCP handler functions (11 tools: browse_recordings, get_recording, mutate_recording, delete_recording, edit_transcript, upload_recording, process_recording, list_folders, merge_recordings, edit_summary, mutate_folder) -- `server.py` — Python MCP server process (`plaud-mcp` entry point, stdio transport) +- `core/` — shared Plaud domain/client layer (client, session, auth, transport, models, errors, appdata, layout, transcode, query, ai_clients) used by all three surfaces below +- `cli/` — Python CLI (`plaud-tools` / `pt` entry points) plus `doctor` diagnostics +- `mcp_pt/` — MCP handler functions (11 tools: browse_recordings, get_recording, mutate_recording, delete_recording, edit_transcript, upload_recording, process_recording, list_folders, merge_recordings, edit_summary, mutate_folder) and the MCP server process (`plaud-mcp` entry point, stdio transport). Named `mcp_pt` rather than `mcp` to avoid shadowing the `mcp` SDK package these modules import. +- `tray/` — Windows tray app, updater/uninstaller, and first-run setup (`plaud-tray` entry point) The TypeScript prior art has been removed. The `har-captures/` directory contains live Plaud API traffic captures (gitignored — local reference only). diff --git a/CONTEXT.md b/CONTEXT.md index 1c1eef9..5b0d25f 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -48,7 +48,7 @@ against this by raising `PlaudSessionExpiredError` when the UT is within `TOKEN_REFRESH_BUFFER_SECONDS` (24 hours) of expiry — shrunk from an earlier 3-day buffer that burned 10% of every token's life. The tray/HomeWindow separately warn starting `TRAY_EXPIRY_WARNING_DAYS` (5 days) before expiry, -so the warning always precedes the refusal (see `session.py`). +so the warning always precedes the refusal (see `core/session.py`). If a future Plaud API endpoint requires a workspace-scoped token, this model needs to grow: store all three tokens, exchange via the workspace @@ -66,7 +66,7 @@ endpoint, refresh on demand. Until then, simplicity wins. **Install layout** — the on-disk arrangement of binaries for the *running* plaud-tools install, derived from `sys.executable`. Distribution-channel -aware (bundle / pip / dev). Represented by `InstallLayout` in `layout.py`. +aware (bundle / pip / dev). Represented by `InstallLayout` in `core/layout.py`. Tray uninstall, tray update, MCP-child process scoping, and AI-client wiring all act on the *running* install, never a hardcoded canonical path. The canonical install path (`%LOCALAPPDATA%\Programs\PlaudTools\`) is the @@ -77,7 +77,7 @@ code. (session storage, tray log, MCP log, events). Channel-agnostic, platform-aware via `platformdirs`. On Windows: `%LOCALAPPDATA%\PlaudTools\`. On macOS / Linux: per `platformdirs.user_data_dir` conventions. Lives in -`appdata.py`. All log and event files share the data directory; logs do +`core/appdata.py`. All log and event files share the data directory; logs do not get a separate `user_log_dir` subtree (deliberate — preserves existing Windows file locations as a no-op and keeps Mac/Linux conventions simple). @@ -86,12 +86,12 @@ Windows file locations as a no-op and keeps Mac/Linux conventions simple). The CLI and MCP surfaces apply different but complementary mechanisms to guard destructive operations: -**CLI (`cli.py`)** — interactive sessions use a `--yes` / `-y` flag. Without +**CLI (`cli/cli.py`)** — interactive sessions use a `--yes` / `-y` flag. Without the flag, a destructive subcommand (e.g. `plaud delete`) prints a confirmation prompt and exits; with `--yes` it proceeds immediately. This is appropriate for terminal users who can read and respond to stdout. -**MCP (`server.py` + `mcp.py`)** — the MCP server runs over stdio and cannot +**MCP (`mcp_pt/server.py` + `mcp_pt/mcp.py`)** — the MCP server runs over stdio and cannot display interactive prompts. Safety is layered: 1. `ToolAnnotations` on every `types.Tool` entry in `_TOOLS` declare diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1bbd5ec..c98d8e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ The default suite is offline and runs in a few seconds. `tests/data/tool_descriptions.golden.json` is a snapshot of every MCP tool's name, description, and `inputSchema`. If you intentionally change any tool -definition in `server.py`, regenerate it and commit the updated fixture +definition in `mcp_pt/server.py`, regenerate it and commit the updated fixture alongside your code change: ``` diff --git a/docs/adr/003-mcp-process-lifecycle.md b/docs/adr/003-mcp-process-lifecycle.md index 4e4a3c8..ef98eb8 100644 --- a/docs/adr/003-mcp-process-lifecycle.md +++ b/docs/adr/003-mcp-process-lifecycle.md @@ -156,7 +156,8 @@ unexpected DLL load failure). **CI frozen-import proof:** The `bundle-smoke` job in `.github/workflows/ci.yml` now builds the tray spec and runs `PlaudTools.exe --diagnose-enum`. This flag is implemented in -`scripts/plaud_tray_entry.py`; it imports `plaud_tools.mcp_lifecycle`, reports +`scripts/plaud_tray_entry.py`; it imports `plaud_tools.cli.process_probe` (renamed +from `plaud_tools.mcp_lifecycle` in the src/ package-by-surface reorg), reports `enumerator=psutil` when psutil is importable in the frozen context, and exits 0 without starting the tkinter/pystray GUI (critical: CI runners have no display). The CI step asserts the output contains `enumerator=psutil`; any regression diff --git a/pyinstaller/plaud-tray.spec b/pyinstaller/plaud-tray.spec index 644f838..5609700 100644 --- a/pyinstaller/plaud-tray.spec +++ b/pyinstaller/plaud-tray.spec @@ -56,8 +56,8 @@ block_cipher = None src = Path(SPECPATH).parent / 'src' _assets = str(Path(SPECPATH).parent / 'src' / 'plaud_tools' / 'assets') -_completions = str(Path(SPECPATH).parent / 'src' / 'plaud_tools' / 'completions') -_scripts = str(Path(SPECPATH).parent / 'src' / 'plaud_tools' / 'scripts') +_completions = str(Path(SPECPATH).parent / 'src' / 'plaud_tools' / 'tray' / 'completions') +_scripts = str(Path(SPECPATH).parent / 'src' / 'plaud_tools' / 'tray' / 'scripts') _icon = str(Path(SPECPATH).parent / 'src' / 'plaud_tools' / 'assets' / 'icon.ico') # sv_ttk ships a Tcl theme file (sun-valley.tcl + sibling .tcl files) as @@ -88,7 +88,7 @@ a = Analysis( 'keyring.backends.fail', 'keyring.core', # ai_clients imported inside tray.background at module level - 'plaud_tools.ai_clients', + 'plaud_tools.core.ai_clients', # sv_ttk applies a Tcl theme at runtime 'sv_ttk', # pywin32 / win32 used by keyring and pystray on Windows diff --git a/pyinstaller/plaud.spec b/pyinstaller/plaud.spec index d0426e6..0baa7d0 100644 --- a/pyinstaller/plaud.spec +++ b/pyinstaller/plaud.spec @@ -28,7 +28,7 @@ a = Analysis( datas=[*_plaud_metadata], hiddenimports=[ # transcode is imported inside an if-branch; static analysis misses it - 'plaud_tools.transcode', + 'plaud_tools.core.transcode', # keyring selects its backend at runtime 'keyring.backends.Windows', 'keyring.backends.fail', diff --git a/pyproject.toml b/pyproject.toml index f8f44fa..cf62b21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,9 +43,9 @@ Issues = "https://github.com/massive-value/plaud-tools/issues" Changelog = "https://github.com/massive-value/plaud-tools/blob/master/CHANGELOG.md" [project.scripts] -plaud-tools = "plaud_tools.cli:main" -pt = "plaud_tools.cli:main" -plaud-mcp = "plaud_tools.server:main" +plaud-tools = "plaud_tools.cli.cli:main" +pt = "plaud_tools.cli.cli:main" +plaud-mcp = "plaud_tools.mcp_pt.server:main" plaud-tray = "plaud_tools.tray.app:main" [tool.setuptools] @@ -160,10 +160,10 @@ disallow_untyped_defs = false # package. Stricter checking here gives the most signal-to-noise benefit. [[tool.mypy.overrides]] module = [ - "plaud_tools.session", - "plaud_tools.client", - "plaud_tools.layout", - "plaud_tools.appdata", + "plaud_tools.core.session", + "plaud_tools.core.client", + "plaud_tools.core.layout", + "plaud_tools.core.appdata", ] disallow_untyped_defs = true warn_return_any = true diff --git a/scripts/ffmpeg_smoke.py b/scripts/ffmpeg_smoke.py index a158c8a..e117830 100644 --- a/scripts/ffmpeg_smoke.py +++ b/scripts/ffmpeg_smoke.py @@ -67,7 +67,7 @@ def main() -> int: # 2. Transcode to MP3 via the real ffmpeg subprocess (path-in/path-out — # the same function production upload flows use). - from plaud_tools.transcode import transcode_to_mp3_path + from plaud_tools.core.transcode import transcode_to_mp3_path tmp_dir = Path(tempfile.mkdtemp(prefix="plaud-ffmpeg-smoke-")) src_path = tmp_dir / "silence.wav" diff --git a/scripts/plaud_entry.py b/scripts/plaud_entry.py index fe83e07..2e7e95b 100644 --- a/scripts/plaud_entry.py +++ b/scripts/plaud_entry.py @@ -1,3 +1,3 @@ -from plaud_tools.cli import main +from plaud_tools.cli.cli import main raise SystemExit(main()) diff --git a/scripts/plaud_mcp_entry.py b/scripts/plaud_mcp_entry.py index 9257d25..7152f69 100644 --- a/scripts/plaud_mcp_entry.py +++ b/scripts/plaud_mcp_entry.py @@ -1,3 +1,3 @@ -from plaud_tools.server import main +from plaud_tools.mcp_pt.server import main main() diff --git a/scripts/plaud_tray_entry.py b/scripts/plaud_tray_entry.py index 25489e5..b89bb7a 100644 --- a/scripts/plaud_tray_entry.py +++ b/scripts/plaud_tray_entry.py @@ -3,7 +3,7 @@ # --------------------------------------------------------------------------- # --diagnose-enum: frozen-import proof for Wave 2 / C4 (psutil in bundle). # -# When this flag is present the entry point imports plaud_tools.mcp_lifecycle +# When this flag is present the entry point imports plaud_tools.cli.process_probe # and reports which process enumerator is active (psutil vs PowerShell # fallback), then exits 0 WITHOUT importing pystray / PIL / tkinter or # launching any GUI. This is intentional: CI runners have no display, so @@ -31,12 +31,12 @@ _enumerator = "psutil" if _psutil_available else "powershell_fallback" print(f"enumerator={_enumerator}", flush=True) - # Also import mcp_lifecycle to ensure its full import chain resolves + # Also import process_probe to ensure its full import chain resolves # in the frozen build (exercises the hiddenimport entries in the spec). try: - import plaud_tools.mcp_lifecycle as _ml # noqa: F401 + import plaud_tools.cli.process_probe as _ml # noqa: F401 except Exception as _exc: - print(f"mcp_lifecycle import failed: {_exc}", flush=True) + print(f"process_probe import failed: {_exc}", flush=True) sys.exit(1) sys.exit(0) @@ -55,7 +55,7 @@ # the tray. if "--com-activate" not in sys.argv: try: - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore SessionStore().prime_dpapi_shadow() except Exception: diff --git a/src/plaud_tools/__init__.py b/src/plaud_tools/__init__.py index 143d30f..40ef12d 100644 --- a/src/plaud_tools/__init__.py +++ b/src/plaud_tools/__init__.py @@ -1,11 +1,11 @@ from importlib.metadata import PackageNotFoundError from importlib.metadata import version as _pkg_version -from .auth import PlaudAuth -from .client import PlaudClient, PlaudRecordingQuery -from .errors import PlaudApiError, PlaudSessionExpiredError -from .mcp import build_handlers -from .session import FileSessionStore, PlaudSession, SessionManager, SessionStore +from .core.auth import PlaudAuth +from .core.client import PlaudClient, PlaudRecordingQuery +from .core.errors import PlaudApiError, PlaudSessionExpiredError +from .core.session import FileSessionStore, PlaudSession, SessionManager, SessionStore +from .mcp_pt.mcp import build_handlers try: __version__ = _pkg_version("plaud-tools") diff --git a/src/plaud_tools/__main__.py b/src/plaud_tools/__main__.py index eb53e2f..1a3b199 100644 --- a/src/plaud_tools/__main__.py +++ b/src/plaud_tools/__main__.py @@ -1,3 +1,3 @@ -from .cli import main +from .cli.cli import main raise SystemExit(main()) diff --git a/src/plaud_tools/cli/__init__.py b/src/plaud_tools/cli/__init__.py new file mode 100644 index 0000000..c05490e --- /dev/null +++ b/src/plaud_tools/cli/__init__.py @@ -0,0 +1 @@ +"""Python CLI (`plaud-tools` / `pt` entry points).""" diff --git a/src/plaud_tools/cli.py b/src/plaud_tools/cli/cli.py similarity index 98% rename from src/plaud_tools/cli.py rename to src/plaud_tools/cli/cli.py index 5f14261..b4b8957 100644 --- a/src/plaud_tools/cli.py +++ b/src/plaud_tools/cli/cli.py @@ -8,11 +8,11 @@ from pathlib import Path from typing import Any -from . import __version__ -from .auth import PlaudAuth -from .client import PlaudClient, PlaudRecordingQuery -from .errors import PlaudApiError, PlaudSessionExpiredError -from .query import ( +from .. import __version__ +from ..core.auth import PlaudAuth +from ..core.client import PlaudClient, PlaudRecordingQuery +from ..core.errors import PlaudApiError, PlaudSessionExpiredError +from ..core.query import ( BROWSE_PAGE_SIZE, collect_filtered_paged, detail_summary_dict, @@ -20,7 +20,7 @@ parse_isoish, summarize_recording, ) -from .session import PlaudSession, SessionManager, SessionStore +from ..core.session import PlaudSession, SessionManager, SessionStore def build_parser() -> argparse.ArgumentParser: @@ -625,7 +625,7 @@ def _handle_set_summary(args: argparse.Namespace, client: PlaudClient) -> str: def _handle_upload(args: argparse.Namespace, client: PlaudClient) -> str: - from .transcode import upload_with_transcode + from ..core.transcode import upload_with_transcode path = Path(args.file) title = args.title or path.stem diff --git a/src/plaud_tools/doctor.py b/src/plaud_tools/cli/doctor.py similarity index 95% rename from src/plaud_tools/doctor.py rename to src/plaud_tools/cli/doctor.py index 9438520..b714668 100644 --- a/src/plaud_tools/doctor.py +++ b/src/plaud_tools/cli/doctor.py @@ -13,14 +13,14 @@ from pathlib import Path from typing import Any -from . import __version__ -from . import ai_clients as _ai_clients_mod -from .ai_clients import CLIENTS -from .appdata import tray_log as _log_path -from .errors import PlaudSessionExpiredError -from .layout import InstallLayout -from .mcp_lifecycle import active_enumerator_name -from .session import SessionManager, SessionStore +from .. import __version__ +from ..core import ai_clients as _ai_clients_mod +from ..core.ai_clients import CLIENTS +from ..core.appdata import tray_log as _log_path +from ..core.errors import PlaudSessionExpiredError +from ..core.layout import InstallLayout +from ..core.session import SessionManager, SessionStore +from .process_probe import active_enumerator_name # --------------------------------------------------------------------------- # Install-dir / executable resolution (delegated to InstallLayout) diff --git a/src/plaud_tools/mcp_lifecycle.py b/src/plaud_tools/cli/process_probe.py similarity index 95% rename from src/plaud_tools/mcp_lifecycle.py rename to src/plaud_tools/cli/process_probe.py index 956644f..899e3ca 100644 --- a/src/plaud_tools/mcp_lifecycle.py +++ b/src/plaud_tools/cli/process_probe.py @@ -1,4 +1,4 @@ -"""MCP process-enumerator introspection. +"""Process-enumerator introspection for `doctor` (formerly `mcp_lifecycle.py`). Historically this module also contained a scoped-shutdown helper and two PowerShell-snippet generators (ADR 003). An audit (2026-07-06) found those diff --git a/src/plaud_tools/core/__init__.py b/src/plaud_tools/core/__init__.py new file mode 100644 index 0000000..0021b8e --- /dev/null +++ b/src/plaud_tools/core/__init__.py @@ -0,0 +1 @@ +"""Shared domain/client layer used by the cli, mcp_pt, and tray surfaces.""" diff --git a/src/plaud_tools/ai_clients.py b/src/plaud_tools/core/ai_clients.py similarity index 100% rename from src/plaud_tools/ai_clients.py rename to src/plaud_tools/core/ai_clients.py diff --git a/src/plaud_tools/appdata.py b/src/plaud_tools/core/appdata.py similarity index 100% rename from src/plaud_tools/appdata.py rename to src/plaud_tools/core/appdata.py diff --git a/src/plaud_tools/auth.py b/src/plaud_tools/core/auth.py similarity index 100% rename from src/plaud_tools/auth.py rename to src/plaud_tools/core/auth.py diff --git a/src/plaud_tools/client.py b/src/plaud_tools/core/client.py similarity index 100% rename from src/plaud_tools/client.py rename to src/plaud_tools/core/client.py diff --git a/src/plaud_tools/errors.py b/src/plaud_tools/core/errors.py similarity index 100% rename from src/plaud_tools/errors.py rename to src/plaud_tools/core/errors.py diff --git a/src/plaud_tools/layout.py b/src/plaud_tools/core/layout.py similarity index 100% rename from src/plaud_tools/layout.py rename to src/plaud_tools/core/layout.py diff --git a/src/plaud_tools/models.py b/src/plaud_tools/core/models.py similarity index 100% rename from src/plaud_tools/models.py rename to src/plaud_tools/core/models.py diff --git a/src/plaud_tools/query.py b/src/plaud_tools/core/query.py similarity index 100% rename from src/plaud_tools/query.py rename to src/plaud_tools/core/query.py diff --git a/src/plaud_tools/session.py b/src/plaud_tools/core/session.py similarity index 100% rename from src/plaud_tools/session.py rename to src/plaud_tools/core/session.py diff --git a/src/plaud_tools/transcode.py b/src/plaud_tools/core/transcode.py similarity index 100% rename from src/plaud_tools/transcode.py rename to src/plaud_tools/core/transcode.py diff --git a/src/plaud_tools/transport.py b/src/plaud_tools/core/transport.py similarity index 100% rename from src/plaud_tools/transport.py rename to src/plaud_tools/core/transport.py diff --git a/src/plaud_tools/mcp_pt/__init__.py b/src/plaud_tools/mcp_pt/__init__.py new file mode 100644 index 0000000..6056ecd --- /dev/null +++ b/src/plaud_tools/mcp_pt/__init__.py @@ -0,0 +1 @@ +"""MCP server process and tool handlers (`plaud-mcp` entry point).""" diff --git a/src/plaud_tools/mcp.py b/src/plaud_tools/mcp_pt/mcp.py similarity index 99% rename from src/plaud_tools/mcp.py rename to src/plaud_tools/mcp_pt/mcp.py index 8938ae2..86b80cc 100644 --- a/src/plaud_tools/mcp.py +++ b/src/plaud_tools/mcp_pt/mcp.py @@ -8,10 +8,10 @@ from pathlib import Path from typing import Any -from .appdata import events_path as _events_path -from .client import PlaudClient, PlaudRecordingQuery -from .errors import PlaudApiError, PlaudSessionExpiredError -from .query import ( +from ..core.appdata import events_path as _events_path +from ..core.client import PlaudClient, PlaudRecordingQuery +from ..core.errors import PlaudApiError, PlaudSessionExpiredError +from ..core.query import ( BROWSE_PAGE_SIZE, collect_filtered_paged, detail_summary_dict, @@ -19,7 +19,7 @@ parse_isoish, summarize_recording, ) -from .session import SessionManager, SessionStore +from ..core.session import SessionManager, SessionStore log = logging.getLogger(__name__) @@ -79,7 +79,7 @@ def _diagnose_session_state() -> dict[str, Any]: """ # Lazy import to avoid the circular import surfaced by # ``plaud_tools/__init__.py`` re-exporting ``build_handlers`` from this module. - from . import __version__ as _app_version + from .. import __version__ as _app_version diag: dict[str, Any] = { "mcp_pid": os.getpid(), @@ -611,7 +611,7 @@ def upload_recording( timezone_offset: float | None = None, ) -> dict[str, Any]: def inner(client: PlaudClient) -> dict[str, Any]: - from .transcode import upload_with_transcode + from ..core.transcode import upload_with_transcode path = Path(file_path) rec_title = title or path.stem diff --git a/src/plaud_tools/server.py b/src/plaud_tools/mcp_pt/server.py similarity index 99% rename from src/plaud_tools/server.py rename to src/plaud_tools/mcp_pt/server.py index b849a92..5ea66d3 100644 --- a/src/plaud_tools/server.py +++ b/src/plaud_tools/mcp_pt/server.py @@ -16,11 +16,11 @@ from mcp.server.lowlevel import NotificationOptions, Server from mcp.server.models import InitializationOptions -from . import __version__ -from .appdata import mcp_log as _mcp_log_path -from .client import PlaudClient +from .. import __version__ +from ..core.appdata import mcp_log as _mcp_log_path +from ..core.client import PlaudClient +from ..core.session import SessionManager, SessionStore from .mcp import build_handlers -from .session import SessionManager, SessionStore def _setup_mcp_logging() -> None: diff --git a/src/plaud_tools/tray/app.py b/src/plaud_tools/tray/app.py index ef17980..8015c26 100644 --- a/src/plaud_tools/tray/app.py +++ b/src/plaud_tools/tray/app.py @@ -23,9 +23,9 @@ from PIL import Image from .. import __version__ as APP_VERSION -from ..client import PlaudClient -from ..errors import PlaudSessionExpiredError -from ..session import TRAY_EXPIRY_WARNING_DAYS, PlaudSession, SessionManager, SessionStore +from ..core.client import PlaudClient +from ..core.errors import PlaudSessionExpiredError +from ..core.session import TRAY_EXPIRY_WARNING_DAYS, PlaudSession, SessionManager, SessionStore from .background import _BackgroundMixin from .icons import _load_icon, _load_icons from .setup import ( @@ -293,7 +293,7 @@ def _open_repo(self) -> None: self._open_url(REPO_URL) def _open_log_folder(self) -> None: - from ..appdata import data_dir as _data_dir + from ..core.appdata import data_dir as _data_dir log_dir = _data_dir() log_dir.mkdir(parents=True, exist_ok=True) diff --git a/src/plaud_tools/tray/background.py b/src/plaud_tools/tray/background.py index 10961d9..b00ea3b 100644 --- a/src/plaud_tools/tray/background.py +++ b/src/plaud_tools/tray/background.py @@ -16,7 +16,7 @@ from collections.abc import Callable from typing import TYPE_CHECKING -from ..ai_clients import connect_all, status_all +from ..core.ai_clients import connect_all, status_all from .setup import ( _ACTIVATE_EVENT, EnvStatus, @@ -34,7 +34,7 @@ if TYPE_CHECKING: import tkinter as tk - from ..session import PlaudSession + from ..core.session import PlaudSession from .windows.home import HomeWindow from .windows.login import LoginWindow diff --git a/src/plaud_tools/completions/_plaud_tools b/src/plaud_tools/tray/completions/_plaud_tools similarity index 100% rename from src/plaud_tools/completions/_plaud_tools rename to src/plaud_tools/tray/completions/_plaud_tools diff --git a/src/plaud_tools/completions/plaud-tools.bash b/src/plaud_tools/tray/completions/plaud-tools.bash similarity index 100% rename from src/plaud_tools/completions/plaud-tools.bash rename to src/plaud_tools/tray/completions/plaud-tools.bash diff --git a/src/plaud_tools/completions/plaud-tools.ps1 b/src/plaud_tools/tray/completions/plaud-tools.ps1 similarity index 100% rename from src/plaud_tools/completions/plaud-tools.ps1 rename to src/plaud_tools/tray/completions/plaud-tools.ps1 diff --git a/src/plaud_tools/ps1_templates.py b/src/plaud_tools/tray/ps1_templates.py similarity index 94% rename from src/plaud_tools/ps1_templates.py rename to src/plaud_tools/tray/ps1_templates.py index 79a077b..cb8211a 100644 --- a/src/plaud_tools/ps1_templates.py +++ b/src/plaud_tools/tray/ps1_templates.py @@ -1,8 +1,8 @@ """Helpers for locating and rendering the bundled PS1 update/uninstall scripts. -The scripts live at ``src/plaud_tools/scripts/{update,uninstall}.ps1`` in the -source tree, and are shipped into the bundle under a ``scripts/`` directory -relative to ``sys._MEIPASS`` (PyInstaller onedir). +The scripts live at ``src/plaud_tools/tray/scripts/{update,uninstall}.ps1`` in +the source tree, and are shipped into the bundle under a ``scripts/`` +directory relative to ``sys._MEIPASS`` (PyInstaller onedir). Public API ---------- @@ -51,7 +51,7 @@ def scripts_dir() -> Path: if (c / "update.ps1").exists(): return c return candidates[0] - # Dev / editable install: scripts live next to this file in scripts/ + # Dev / editable install: scripts live in tray/scripts/, next to this file. return Path(__file__).parent / "scripts" diff --git a/src/plaud_tools/scripts/uninstall.ps1 b/src/plaud_tools/tray/scripts/uninstall.ps1 similarity index 100% rename from src/plaud_tools/scripts/uninstall.ps1 rename to src/plaud_tools/tray/scripts/uninstall.ps1 diff --git a/src/plaud_tools/scripts/update.ps1 b/src/plaud_tools/tray/scripts/update.ps1 similarity index 100% rename from src/plaud_tools/scripts/update.ps1 rename to src/plaud_tools/tray/scripts/update.ps1 diff --git a/src/plaud_tools/tray/setup.py b/src/plaud_tools/tray/setup.py index 0a1199f..e576176 100644 --- a/src/plaud_tools/tray/setup.py +++ b/src/plaud_tools/tray/setup.py @@ -10,9 +10,9 @@ from pathlib import Path from typing import TypeGuard, TypeVar -from ..appdata import events_path as _events_path -from ..appdata import tray_log as _tray_log_path -from ..layout import InstallLayout +from ..core.appdata import events_path as _events_path +from ..core.appdata import tray_log as _tray_log_path +from ..core.layout import InstallLayout APP_NAME = "Plaud Tools" diff --git a/src/plaud_tools/tray/uninstaller.py b/src/plaud_tools/tray/uninstaller.py index b2612bd..3f8ca90 100644 --- a/src/plaud_tools/tray/uninstaller.py +++ b/src/plaud_tools/tray/uninstaller.py @@ -10,12 +10,12 @@ from pathlib import Path from tkinter import ttk -from ..appdata import data_dir as _data_dir -from ..layout import InstallLayout -from ..ps1_templates import render_uninstall_ps1 -from ..session import SessionStore +from ..core.appdata import data_dir as _data_dir +from ..core.layout import InstallLayout +from ..core.session import SessionStore from .process_launch import POWERSHELL_EXE as _POWERSHELL_EXE from .process_launch import launch_hidden_powershell +from .ps1_templates import render_uninstall_ps1 from .setup import ( APP_NAME, _cli_dir, @@ -295,7 +295,7 @@ def do_uninstall() -> None: # Execute simple removals first. if var_clients.get(): try: - from ..ai_clients import disconnect_all + from ..core.ai_clients import disconnect_all disconnect_all() logging.info("Disconnected AI clients") diff --git a/src/plaud_tools/tray/updater.py b/src/plaud_tools/tray/updater.py index ae633aa..8861f7c 100644 --- a/src/plaud_tools/tray/updater.py +++ b/src/plaud_tools/tray/updater.py @@ -18,10 +18,10 @@ from typing import TYPE_CHECKING from .. import __version__ as APP_VERSION -from ..layout import InstallLayout -from ..ps1_templates import render_update_ps1 +from ..core.layout import InstallLayout from .process_launch import _CREATE_BREAKAWAY_FROM_JOB, launch_hidden_powershell from .process_launch import POWERSHELL_EXE as _POWERSHELL_EXE +from .ps1_templates import render_update_ps1 from .setup import APP_NAME, _configure_if_alive, _set_app_icon if TYPE_CHECKING: # pragma: no cover diff --git a/src/plaud_tools/tray/windows/login.py b/src/plaud_tools/tray/windows/login.py index b072a96..a3e7dfa 100644 --- a/src/plaud_tools/tray/windows/login.py +++ b/src/plaud_tools/tray/windows/login.py @@ -8,9 +8,9 @@ from collections.abc import Callable from tkinter import ttk -from ...auth import PlaudAuth -from ...errors import PlaudApiError, PlaudSessionExpiredError -from ...session import SessionStore +from ...core.auth import PlaudAuth +from ...core.errors import PlaudApiError, PlaudSessionExpiredError +from ...core.session import SessionStore from ..setup import APP_NAME, _set_app_icon, _widget_alive # Plaud's password-based login endpoint returns a bare HTTP 401 for Google-SSO diff --git a/src/plaud_tools/tray/windows/wizard.py b/src/plaud_tools/tray/windows/wizard.py index 63dcfc9..19a2a79 100644 --- a/src/plaud_tools/tray/windows/wizard.py +++ b/src/plaud_tools/tray/windows/wizard.py @@ -6,7 +6,7 @@ from collections.abc import Callable from tkinter import ttk -from ...ai_clients import CLIENTS, connect, disconnect, status_all +from ...core.ai_clients import CLIENTS, connect, disconnect, status_all from ..setup import APP_NAME, _mcp_exe, _set_app_icon _STATUS_BADGE: dict[str, tuple[str, str]] = { diff --git a/tests/conftest.py b/tests/conftest.py index a09d3f4..667a4a1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,7 +35,7 @@ def _zero_keyring_retry_delay(monkeypatch): zero-delay sequence of the appropriate length. """ try: - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore except Exception: return monkeypatch.setattr(SessionStore, "_KEYRING_RETRY_DELAYS_S", (), raising=False) @@ -46,7 +46,7 @@ def _zero_keyring_retry_delay(monkeypatch): # lazy variant of this lookup would resolve to ``None`` inside the fixtures # (the redirect fixture wins) and silently disable the trip-wire. try: - from plaud_tools.appdata import dpapi_shadow_path as _resolve_dpapi_shadow_path + from plaud_tools.core.appdata import dpapi_shadow_path as _resolve_dpapi_shadow_path _REAL_DPAPI_SHADOW_PATH = _resolve_dpapi_shadow_path() except Exception: @@ -67,7 +67,7 @@ def _block_real_dpapi_shadow(monkeypatch): the default to ``None`` here means any future regression that forgets ``dpapi_path=`` writes nothing — it does not corrupt the user's session. """ - monkeypatch.setattr("plaud_tools.appdata.dpapi_shadow_path", lambda: None) + monkeypatch.setattr("plaud_tools.core.appdata.dpapi_shadow_path", lambda: None) @pytest.fixture(autouse=True) @@ -80,7 +80,7 @@ def _block_real_session_path(monkeypatch, tmp_path): session file. Redirect to a per-test tmp directory so accidental omissions fail safely (file simply won't exist) rather than corrupting real state. """ - monkeypatch.setattr("plaud_tools.appdata.session_path", lambda: tmp_path / "session.json") + monkeypatch.setattr("plaud_tools.core.appdata.session_path", lambda: tmp_path / "session.json") @pytest.fixture(autouse=True) diff --git a/tests/test_ai_clients.py b/tests/test_ai_clients.py index 7e8531b..01881f0 100644 --- a/tests/test_ai_clients.py +++ b/tests/test_ai_clients.py @@ -3,7 +3,7 @@ import tomllib from pathlib import Path -from plaud_tools import ai_clients +from plaud_tools.core import ai_clients def test_codex_connect_writes_parsable_toml_with_windows_path(tmp_path: Path, monkeypatch): diff --git a/tests/test_appdata.py b/tests/test_appdata.py index 359ecf9..abc8a31 100644 --- a/tests/test_appdata.py +++ b/tests/test_appdata.py @@ -1,4 +1,4 @@ -"""Tests for plaud_tools.appdata — per-user data directory and known file paths. +"""Tests for plaud_tools.core.appdata — per-user data directory and known file paths. Acceptance criteria from issue #88: - data_dir() returns %LOCALAPPDATA%\\PlaudTools on Windows (exact backwards-compat) @@ -31,7 +31,7 @@ def test_uses_localappdata_env_var(self, monkeypatch, tmp_path): # Re-import to pick up platform patch import importlib - import plaud_tools.appdata as appdata_mod + import plaud_tools.core.appdata as appdata_mod importlib.reload(appdata_mod) result = appdata_mod.data_dir() @@ -44,7 +44,7 @@ def test_localappdata_missing_falls_back_to_home(self, monkeypatch, tmp_path): monkeypatch.setattr(Path, "home", lambda: tmp_path) import importlib - import plaud_tools.appdata as appdata_mod + import plaud_tools.core.appdata as appdata_mod importlib.reload(appdata_mod) result = appdata_mod.data_dir() @@ -58,7 +58,7 @@ def test_macos_uses_platformdirs(self, monkeypatch): fake_dir = "/Users/testuser/Library/Application Support/PlaudTools" import importlib - import plaud_tools.appdata as appdata_mod + import plaud_tools.core.appdata as appdata_mod with patch("platformdirs.user_data_dir", return_value=fake_dir): importlib.reload(appdata_mod) @@ -71,7 +71,7 @@ def test_linux_uses_platformdirs(self, monkeypatch): fake_dir = "/home/testuser/.local/share/PlaudTools" import importlib - import plaud_tools.appdata as appdata_mod + import plaud_tools.core.appdata as appdata_mod with patch("platformdirs.user_data_dir", return_value=fake_dir): importlib.reload(appdata_mod) @@ -96,7 +96,7 @@ def _pin_data_dir(self, monkeypatch, tmp_path): This is safe because data_dir() is pinned to tmp_path — dpapi_shadow_path can never reach the real %LOCALAPPDATA%\\PlaudTools\\ directory. """ - import plaud_tools.appdata as appdata_mod + import plaud_tools.core.appdata as appdata_mod monkeypatch.setattr(appdata_mod, "data_dir", lambda: tmp_path) @@ -110,46 +110,46 @@ def _safe_dpapi_shadow_path(): monkeypatch.setattr(appdata_mod, "dpapi_shadow_path", _safe_dpapi_shadow_path) def test_tray_log_inside_data_dir(self, tmp_path): - from plaud_tools.appdata import tray_log + from plaud_tools.core.appdata import tray_log result = tray_log() assert result == tmp_path / "tray.log" def test_mcp_log_inside_data_dir(self, tmp_path): - from plaud_tools.appdata import mcp_log + from plaud_tools.core.appdata import mcp_log result = mcp_log() assert result == tmp_path / "mcp.log" def test_events_path_inside_data_dir(self, tmp_path): - from plaud_tools.appdata import events_path + from plaud_tools.core.appdata import events_path result = events_path() assert result == tmp_path / "events.jsonl" def test_session_path_inside_data_dir(self, tmp_path): - from plaud_tools.appdata import session_path + from plaud_tools.core.appdata import session_path result = session_path() assert result == tmp_path / "session.json" def test_dpapi_shadow_path_inside_data_dir_on_windows(self, monkeypatch, tmp_path): monkeypatch.setattr(sys, "platform", "win32") - from plaud_tools import appdata as appdata_mod + from plaud_tools.core import appdata as appdata_mod result = appdata_mod.dpapi_shadow_path() assert result == tmp_path / "session.dat" def test_dpapi_shadow_path_returns_none_on_macos(self, monkeypatch, tmp_path): monkeypatch.setattr(sys, "platform", "darwin") - from plaud_tools import appdata as appdata_mod + from plaud_tools.core import appdata as appdata_mod result = appdata_mod.dpapi_shadow_path() assert result is None def test_dpapi_shadow_path_returns_none_on_linux(self, monkeypatch, tmp_path): monkeypatch.setattr(sys, "platform", "linux") - from plaud_tools import appdata as appdata_mod + from plaud_tools.core import appdata as appdata_mod result = appdata_mod.dpapi_shadow_path() assert result is None @@ -171,7 +171,7 @@ def test_exact_path_matches_localappdata_reconstruction(self): """data_dir() == Path(os.environ['LOCALAPPDATA']) / 'PlaudTools' on live Windows.""" import os - from plaud_tools.appdata import data_dir + from plaud_tools.core.appdata import data_dir expected = Path(os.environ["LOCALAPPDATA"]) / "PlaudTools" assert data_dir() == expected diff --git a/tests/test_auth.py b/tests/test_auth.py index 944706d..a025cc1 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -4,10 +4,10 @@ import pytest -from plaud_tools.auth import PlaudAuth -from plaud_tools.errors import PlaudApiError -from plaud_tools.session import SessionStore -from plaud_tools.transport import HttpResponse +from plaud_tools.core.auth import PlaudAuth +from plaud_tools.core.errors import PlaudApiError +from plaud_tools.core.session import SessionStore +from plaud_tools.core.transport import HttpResponse # Capture the production retry-delays sequence at import time (before any # monkeypatch can override it on the class). Tests that need the full retry @@ -85,7 +85,7 @@ def test_load_retries_keyring_on_transient_failure(tmp_path, caplog, monkeypatch """ import logging - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore payload = json.dumps({"access_token": "header.payload.sig", "region": "us", "email": "u@example.com"}) call_count = {"n": 0} @@ -99,7 +99,7 @@ def get_password(*_a, **_k): return payload monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: FlakeyKeyring if name == "keyring" else __import__(name), ) # Don't let the retry delay slow the suite down — zero-delay single entry @@ -108,7 +108,7 @@ def get_password(*_a, **_k): store = SessionStore(tmp_path / "session.json", service_name="plaud-tools-test-retry") - with caplog.at_level(logging.WARNING, logger="plaud_tools.session"): + with caplog.at_level(logging.WARNING, logger="plaud_tools.core.session"): session = store.load() assert session is not None, "Expected retry to recover the session" @@ -131,7 +131,7 @@ def test_load_retries_then_falls_through_on_persistent_none(tmp_path, monkeypatc nothing is stored, so every read returns None and we expect exactly the configured number of reads. """ - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore call_count = {"n": 0} @@ -142,7 +142,7 @@ def get_password(*_a, **_k): return None # consistently absent — should settle after the retry budget monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: EmptyKeyring if name == "keyring" else __import__(name), ) # Preserve the full retry count while zeroing delays so the assertion on @@ -150,7 +150,7 @@ def get_password(*_a, **_k): # constant (captured before any monkeypatch) rather than reading the class # attribute, which the autouse fixture has already overridden to ``()``. monkeypatch.setattr( - "plaud_tools.session.SessionStore._KEYRING_RETRY_DELAYS_S", + "plaud_tools.core.session.SessionStore._KEYRING_RETRY_DELAYS_S", (0.0,) * len(_PROD_RETRY_DELAYS_S), ) @@ -170,7 +170,7 @@ def test_load_recovers_from_late_keyring(tmp_path, monkeypatch): import json from dataclasses import asdict - from plaud_tools.session import PlaudSession, SessionStore + from plaud_tools.core.session import PlaudSession, SessionStore session_data = json.dumps(asdict(PlaudSession(access_token="tok", email="cold@example.com"))) call_count = {"n": 0} @@ -183,7 +183,7 @@ def get_password(*_a, **_k): return None if call_count["n"] < settle_after else session_data monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: SlowKeyring if name == "keyring" else __import__(name), ) # settle_after=4 needs at least 3 delays; use a full-length zero sequence @@ -191,7 +191,7 @@ def get_password(*_a, **_k): # module-level constant (captured before any monkeypatch) rather than # reading the class attribute, which the autouse fixture overrides to ``()``. monkeypatch.setattr( - "plaud_tools.session.SessionStore._KEYRING_RETRY_DELAYS_S", + "plaud_tools.core.session.SessionStore._KEYRING_RETRY_DELAYS_S", (0.0,) * len(_PROD_RETRY_DELAYS_S), ) @@ -209,7 +209,7 @@ def test_load_recovers_from_transient_none(tmp_path, monkeypatch): import json from dataclasses import asdict - from plaud_tools.session import PlaudSession, SessionStore + from plaud_tools.core.session import PlaudSession, SessionStore session_data = json.dumps(asdict(PlaudSession(access_token="tok", email="u@example.com"))) call_count = {"n": 0} @@ -221,7 +221,7 @@ def get_password(*_a, **_k): return None if call_count["n"] == 1 else session_data monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: TransientKeyring if name == "keyring" else __import__(name), ) # Single zero-delay entry keeps the attempt count (2) without sleeping. @@ -238,7 +238,7 @@ def test_load_retry_gives_up_after_persistent_failures(tmp_path, caplog, monkeyp """If every attempt raises, surface the failure (return None, fall back to file).""" import logging - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore call_count = {"n": 0} @@ -249,7 +249,7 @@ def get_password(*_a, **_k): raise RuntimeError("persistent failure") monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: BrokenKeyring if name == "keyring" else __import__(name), ) # Preserve the full retry count while zeroing delays so the assertion on @@ -259,7 +259,7 @@ def get_password(*_a, **_k): monkeypatch.setattr(SessionStore, "_KEYRING_RETRY_DELAYS_S", (0.0,) * len(_PROD_RETRY_DELAYS_S)) store = SessionStore(tmp_path / "session.json", service_name="plaud-tools-test-broken") - with caplog.at_level(logging.WARNING, logger="plaud_tools.session"): + with caplog.at_level(logging.WARNING, logger="plaud_tools.core.session"): assert store.load() is None assert call_count["n"] == (len(_PROD_RETRY_DELAYS_S) + 1) @@ -274,7 +274,7 @@ def test_save_logs_warning_on_keyring_failure(tmp_path, caplog, monkeypatch): """ import logging - from plaud_tools.session import PlaudSession, SessionStore + from plaud_tools.core.session import PlaudSession, SessionStore class BrokenKeyring: @staticmethod @@ -286,14 +286,14 @@ def get_password(*_a, **_k): return None monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: BrokenKeyring if name == "keyring" else __import__(name), ) store = SessionStore(tmp_path / "session.json", service_name="plaud-tools-test-keyring-warn") session = PlaudSession(access_token="header.payload.sig", region="us", email="user@example.com") - with caplog.at_level(logging.WARNING, logger="plaud_tools.session"): + with caplog.at_level(logging.WARNING, logger="plaud_tools.core.session"): store.save(session) # File store wrote the fallback. @@ -360,7 +360,7 @@ def test_legacy_keyring_migration_happy_path(tmp_path, monkeypatch, caplog): """ import logging - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore fake = _FakeLegacyKeyring( { @@ -369,14 +369,14 @@ def test_legacy_keyring_migration_happy_path(tmp_path, monkeypatch, caplog): } ) monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: fake if name == "keyring" else __import__(name), ) monkeypatch.setattr(SessionStore, "_KEYRING_RETRY_DELAYS_S", ()) store = SessionStore(tmp_path / "session.json", service_name="plaud-tools", dpapi_path=None) - with caplog.at_level(logging.INFO, logger="plaud_tools.session"): + with caplog.at_level(logging.INFO, logger="plaud_tools.core.session"): session, source = store.load_with_source() assert session is not None @@ -399,7 +399,7 @@ def test_legacy_keyring_migration_idempotent_on_second_load(tmp_path, monkeypatc """After the first load migrates, a second load reads from the new entry and never touches the legacy slots again. """ - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore fake = _FakeLegacyKeyring( { @@ -408,7 +408,7 @@ def test_legacy_keyring_migration_idempotent_on_second_load(tmp_path, monkeypatc } ) monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: fake if name == "keyring" else __import__(name), ) monkeypatch.setattr(SessionStore, "_KEYRING_RETRY_DELAYS_S", ()) @@ -427,7 +427,7 @@ def test_legacy_keyring_missing_one_half_does_not_migrate(tmp_path, monkeypatch) """If only the jwt entry exists (no profile), no migration happens — we don't want to invent an email/region we don't have evidence for. """ - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore fake = _FakeLegacyKeyring( { @@ -435,7 +435,7 @@ def test_legacy_keyring_missing_one_half_does_not_migrate(tmp_path, monkeypatch) } ) monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: fake if name == "keyring" else __import__(name), ) monkeypatch.setattr(SessionStore, "_KEYRING_RETRY_DELAYS_S", ()) @@ -453,7 +453,7 @@ def test_legacy_keyring_migration_handles_malformed_profile(tmp_path, monkeypatc """A non-JSON profile blob falls back to defaults (region='us', email=None); the JWT still migrates. """ - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore fake = _FakeLegacyKeyring( { @@ -462,7 +462,7 @@ def test_legacy_keyring_migration_handles_malformed_profile(tmp_path, monkeypatc } ) monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: fake if name == "keyring" else __import__(name), ) monkeypatch.setattr(SessionStore, "_KEYRING_RETRY_DELAYS_S", ()) @@ -508,8 +508,8 @@ def unprotect(cls, ciphertext: bytes) -> bytes: def _patch_fake_dpapi(monkeypatch): - monkeypatch.setattr("plaud_tools.session._dpapi_protect", _FakeDpapi.protect) - monkeypatch.setattr("plaud_tools.session._dpapi_unprotect", _FakeDpapi.unprotect) + monkeypatch.setattr("plaud_tools.core.session._dpapi_protect", _FakeDpapi.protect) + monkeypatch.setattr("plaud_tools.core.session._dpapi_unprotect", _FakeDpapi.unprotect) def test_save_writes_keyring_and_dpapi_shadow(tmp_path, monkeypatch): @@ -517,12 +517,12 @@ def test_save_writes_keyring_and_dpapi_shadow(tmp_path, monkeypatch): a subsequent cold-start MCP load has a Credential-Manager-independent path to the session. """ - from plaud_tools.session import PlaudSession, SessionStore + from plaud_tools.core.session import PlaudSession, SessionStore _patch_fake_dpapi(monkeypatch) fake = _FakeLegacyKeyring() monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: fake if name == "keyring" else __import__(name), ) @@ -548,7 +548,7 @@ def test_load_falls_back_to_dpapi_when_keyring_returns_none(tmp_path, monkeypatc """ import logging - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore _patch_fake_dpapi(monkeypatch) @@ -570,7 +570,7 @@ def set_password(*_a, **_k): pass monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: EmptyKeyring if name == "keyring" else __import__(name), ) @@ -581,7 +581,7 @@ def set_password(*_a, **_k): store = SessionStore(tmp_path / "session.json", service_name="plaud-tools", dpapi_path=shadow) - with caplog.at_level(logging.WARNING, logger="plaud_tools.session"): + with caplog.at_level(logging.WARNING, logger="plaud_tools.core.session"): session, source = store.load_with_source() assert session is not None, "DPAPI fallback should have recovered the session" @@ -596,12 +596,12 @@ def test_dpapi_disabled_when_path_is_none(tmp_path, monkeypatch): tests that pin the plaintext file-store fallback and for non-Windows users who want zero filesystem residue beyond the keyring. """ - from plaud_tools.session import PlaudSession, SessionStore + from plaud_tools.core.session import PlaudSession, SessionStore _patch_fake_dpapi(monkeypatch) fake = _FakeLegacyKeyring() monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: fake if name == "keyring" else __import__(name), ) @@ -617,7 +617,7 @@ def test_dpapi_not_enabled_for_non_canonical_service(tmp_path): """Auto-default is gated to service_name=='plaud-tools' so test fixtures using synthetic service names never touch the real user's session.dat. """ - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore store = SessionStore(tmp_path / "session.json", service_name="plaud-tools-test-foo") assert store.dpapi_path is None @@ -627,12 +627,12 @@ def test_clear_removes_dpapi_shadow(tmp_path, monkeypatch): """clear() must wipe the keyring entry AND the DPAPI shadow AND the plaintext file — sign-out should leave no recoverable session anywhere. """ - from plaud_tools.session import PlaudSession, SessionStore + from plaud_tools.core.session import PlaudSession, SessionStore _patch_fake_dpapi(monkeypatch) fake = _FakeLegacyKeyring() monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: fake if name == "keyring" else __import__(name), ) @@ -656,12 +656,12 @@ def test_load_ignores_dpapi_when_decryption_fails(tmp_path, monkeypatch, caplog) """ import logging - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore def _broken_unprotect(_blob): raise OSError("DPAPI decryption failed (simulated)") - monkeypatch.setattr("plaud_tools.session._dpapi_unprotect", _broken_unprotect) + monkeypatch.setattr("plaud_tools.core.session._dpapi_unprotect", _broken_unprotect) shadow = tmp_path / "session.dat" shadow.write_bytes(b"\x01\x02\x03\x04") # bytes the broken unprotect will reject @@ -676,12 +676,12 @@ def get_credential(*_a, **_k): return None monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: EmptyKeyring if name == "keyring" else __import__(name), ) store = SessionStore(tmp_path / "session.json", service_name="plaud-tools", dpapi_path=shadow) - with caplog.at_level(logging.WARNING, logger="plaud_tools.session"): + with caplog.at_level(logging.WARNING, logger="plaud_tools.core.session"): session, source = store.load_with_source() assert session is None @@ -699,7 +699,7 @@ def test_real_dpapi_roundtrip(tmp_path): CI on windows-latest instead of only when a user actually signs in to the frozen bundle. """ - from plaud_tools.session import _dpapi_protect, _dpapi_unprotect + from plaud_tools.core.session import _dpapi_protect, _dpapi_unprotect plaintext = b'{"access_token":"x","region":"us","email":"u@example.com"}' blob = _dpapi_protect(plaintext) @@ -713,7 +713,7 @@ def test_keyring_load_self_heals_missing_dpapi_shadow(tmp_path, monkeypatch): populate the shadow so the fallback is available on the *next* cold-start MCP read — without forcing the user to sign out + sign in. """ - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore _patch_fake_dpapi(monkeypatch) payload = json.dumps({"access_token": "tok", "region": "us", "email": "u@example.com"}) @@ -728,7 +728,7 @@ def get_credential(*_a, **_k): return None monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: HealthyKeyring if name == "keyring" else __import__(name), ) @@ -750,7 +750,7 @@ def test_self_heal_does_not_overwrite_existing_shadow(tmp_path, monkeypatch): that protects a freshly-saved shadow against accidental rewrites from a racing stale keyring read. """ - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore _patch_fake_dpapi(monkeypatch) new_payload = json.dumps({"access_token": "new", "region": "us", "email": "new@example.com"}) @@ -766,7 +766,7 @@ def get_credential(*_a, **_k): return None monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: HealthyKeyring if name == "keyring" else __import__(name), ) @@ -785,7 +785,7 @@ def test_save_succeeds_when_keyring_fails_but_dpapi_works(tmp_path, monkeypatch) closes the door on a security regression where a keyring outage forced us to write the token plaintext to ~/.config/plaud-tools/session.json. """ - from plaud_tools.session import PlaudSession, SessionStore + from plaud_tools.core.session import PlaudSession, SessionStore _patch_fake_dpapi(monkeypatch) @@ -803,7 +803,7 @@ def get_credential(*_a, **_k): return None monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: BrokenKeyring if name == "keyring" else __import__(name), ) @@ -824,7 +824,7 @@ def get_credential(*_a, **_k): def test_prime_writes_shadow_when_keyring_healthy_and_shadow_missing(tmp_path, monkeypatch): - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore _patch_fake_dpapi(monkeypatch) payload = json.dumps({"access_token": "tok", "region": "us", "email": "u@example.com"}) @@ -835,7 +835,7 @@ def get_password(*_a, **_k): return payload monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: HealthyKeyring if name == "keyring" else __import__(name), ) @@ -853,7 +853,7 @@ def test_prime_is_noop_when_shadow_already_exists(tmp_path, monkeypatch): that protects a freshly-saved shadow against an accidental rewrite from a racing keyring read inside prime. """ - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore _patch_fake_dpapi(monkeypatch) sentinel = b"PRE-EXISTING-SHADOW" @@ -867,7 +867,7 @@ def get_password(*_a, **_k): return None monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: TrackingKeyring if name == "keyring" else __import__(name), ) @@ -885,7 +885,7 @@ def test_prime_is_noop_when_keyring_returns_none(tmp_path, monkeypatch): """Signed-out users (keyring legitimately empty) must not be charged any retry budget here — prime must read exactly once and give up. """ - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore _patch_fake_dpapi(monkeypatch) @@ -898,7 +898,7 @@ def get_password(*_a, **_k): return None monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: EmptyKeyring if name == "keyring" else __import__(name), ) @@ -915,7 +915,7 @@ def test_prime_swallows_keyring_exception(tmp_path, monkeypatch): the tray entry script wraps prime in its own try/except but the contract is that prime itself is fully best-effort. """ - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore _patch_fake_dpapi(monkeypatch) @@ -925,7 +925,7 @@ def get_password(*_a, **_k): raise RuntimeError("vault locked") monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: BrokenKeyring if name == "keyring" else __import__(name), ) @@ -937,7 +937,7 @@ def get_password(*_a, **_k): def test_prime_is_noop_when_dpapi_disabled(tmp_path, monkeypatch): - from plaud_tools.session import SessionStore + from plaud_tools.core.session import SessionStore _patch_fake_dpapi(monkeypatch) payload = json.dumps({"access_token": "tok", "region": "us", "email": "u@example.com"}) @@ -948,7 +948,7 @@ def get_password(*_a, **_k): return payload monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: HealthyKeyring if name == "keyring" else __import__(name), ) @@ -973,8 +973,8 @@ def test_file_session_store_default_path_uses_appdata_session_path(monkeypatch, We verify the resolved path equals whatever appdata.session_path() returns in this context (which is the redirected tmp path). """ - from plaud_tools import appdata as appdata_mod - from plaud_tools.session import FileSessionStore + from plaud_tools.core import appdata as appdata_mod + from plaud_tools.core.session import FileSessionStore expected = appdata_mod.session_path() store = FileSessionStore() @@ -998,8 +998,8 @@ def test_session_store_default_dpapi_path_uses_appdata_on_windows(monkeypatch, t """ import sys as _sys - from plaud_tools import appdata as appdata_mod - from plaud_tools.session import SessionStore + from plaud_tools.core import appdata as appdata_mod + from plaud_tools.core.session import SessionStore # Simulate Windows: patch appdata.dpapi_shadow_path to return a known path fake_shadow = tmp_path / "fake_shadow.dat" @@ -1019,8 +1019,8 @@ def test_session_store_default_dpapi_path_is_none_off_windows(monkeypatch, tmp_p """ import sys as _sys - from plaud_tools import appdata as appdata_mod - from plaud_tools.session import SessionStore + from plaud_tools.core import appdata as appdata_mod + from plaud_tools.core.session import SessionStore monkeypatch.setattr(_sys, "platform", "linux") monkeypatch.setattr(appdata_mod, "dpapi_shadow_path", lambda: None) @@ -1039,8 +1039,8 @@ def test_session_store_default_dpapi_path_is_none_off_windows(monkeypatch, tmp_p def test_file_session_store_load_clean_install(tmp_path, monkeypatch): """Clean install: neither new path nor legacy path exists -> returns None.""" - from plaud_tools import appdata as appdata_mod - from plaud_tools.session import FileSessionStore + from plaud_tools.core import appdata as appdata_mod + from plaud_tools.core.session import FileSessionStore new_path = tmp_path / "new" / "session.json" monkeypatch.setattr(appdata_mod, "session_path", lambda: new_path) @@ -1055,8 +1055,8 @@ def test_file_session_store_load_upgrade_with_legacy_file(tmp_path, monkeypatch) """ import json as _json - from plaud_tools import appdata as appdata_mod - from plaud_tools.session import FileSessionStore + from plaud_tools.core import appdata as appdata_mod + from plaud_tools.core.session import FileSessionStore new_path = tmp_path / "new" / "session.json" legacy_dir = tmp_path / "legacy" / ".config" / "plaud-tools" @@ -1096,8 +1096,8 @@ def test_file_session_store_load_upgrade_new_path_wins(tmp_path, monkeypatch): """ import json as _json - from plaud_tools import appdata as appdata_mod - from plaud_tools.session import FileSessionStore + from plaud_tools.core import appdata as appdata_mod + from plaud_tools.core.session import FileSessionStore new_path = tmp_path / "new" / "session.json" new_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/tests/test_client.py b/tests/test_client.py index 16c73af..dd402c4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -9,11 +9,11 @@ import pytest -import plaud_tools.client as client_mod -from plaud_tools.client import PlaudClient, PlaudRecordingQuery -from plaud_tools.errors import PlaudApiError, PlaudSessionExpiredError -from plaud_tools.session import FileSessionStore, PlaudSession, SessionManager, SessionStore -from plaud_tools.transport import HttpResponse +import plaud_tools.core.client as client_mod +from plaud_tools.core.client import PlaudClient, PlaudRecordingQuery +from plaud_tools.core.errors import PlaudApiError, PlaudSessionExpiredError +from plaud_tools.core.session import FileSessionStore, PlaudSession, SessionManager, SessionStore +from plaud_tools.core.transport import HttpResponse class StubTransport: @@ -1234,7 +1234,7 @@ def set_password(service, account, value): def get_password(service, account): return FakeKeyring.value - monkeypatch.setattr("plaud_tools.session.importlib.import_module", lambda name: FakeKeyring) + monkeypatch.setattr("plaud_tools.core.session.importlib.import_module", lambda name: FakeKeyring) # dpapi_path MUST be pinned under tmp_path. Leaving it unset on Windows # makes appdata.dpapi_shadow_path() resolve to the real # %LOCALAPPDATA%\PlaudTools\session.dat, so store.save() DPAPI-encrypts @@ -1253,7 +1253,7 @@ def test_session_store_falls_back_to_file_when_keyring_unavailable(tmp_path, mon def raise_import_error(name): raise ImportError(name) - monkeypatch.setattr("plaud_tools.session.importlib.import_module", raise_import_error) + monkeypatch.setattr("plaud_tools.core.session.importlib.import_module", raise_import_error) # Explicitly disable DPAPI for this test — we are pinning the plaintext # file-store fallback that fires when *every* OS-protected path is # unavailable. The DPAPI path has its own dedicated tests. diff --git a/tests/test_conftest_safety.py b/tests/test_conftest_safety.py index 442a8da..ce5c930 100644 --- a/tests/test_conftest_safety.py +++ b/tests/test_conftest_safety.py @@ -22,7 +22,7 @@ from pathlib import Path -from plaud_tools import appdata +from plaud_tools.core import appdata def test_dpapi_shadow_path_is_redirected_to_none(): diff --git a/tests/test_diagnose_enum.py b/tests/test_diagnose_enum.py index e7168da..2610d59 100644 --- a/tests/test_diagnose_enum.py +++ b/tests/test_diagnose_enum.py @@ -5,7 +5,7 @@ - exits 0 - does NOT import pystray, PIL, or tkinter (GUI modules must stay out of the --diagnose-enum code path so CI runners without a display are safe) - - imports plaud_tools.mcp_lifecycle successfully (exercises the module's + - imports plaud_tools.cli.process_probe successfully (exercises the module's import chain in a non-frozen context as a unit-level smoke test) The actual frozen-build assertion ("enumerator=psutil") is performed by the @@ -185,23 +185,23 @@ def test_diagnose_enum_does_not_import_gui_modules(): ) -def test_diagnose_enum_imports_mcp_lifecycle(): - """--diagnose-enum must import plaud_tools.mcp_lifecycle without error. +def test_diagnose_enum_imports_process_probe(): + """--diagnose-enum must import plaud_tools.cli.process_probe without error. - This is the chain exercised by the frozen hiddenimports: mcp_lifecycle + This is the chain exercised by the frozen hiddenimports: process_probe imports psutil (or falls back), and the flag verifies the import resolves. - If mcp_lifecycle import fails the flag exits 1 and prints an error line — + If process_probe import fails the flag exits 1 and prints an error line — we assert it does NOT print a failure line and exits 0. """ result = _run_entry_with_flag("--diagnose-enum") assert result.returncode == 0, ( - f"Expected exit 0 (mcp_lifecycle imported OK), " + f"Expected exit 0 (process_probe imported OK), " f"got {result.returncode}.\n" f"stdout: {result.stdout!r}\n" f"stderr: {result.stderr!r}" ) - assert "mcp_lifecycle import failed" not in result.stdout, ( - f"mcp_lifecycle import failure detected in output: {result.stdout!r}" + assert "process_probe import failed" not in result.stdout, ( + f"process_probe import failure detected in output: {result.stdout!r}" ) diff --git a/tests/test_doctor.py b/tests/test_doctor.py index 2b498c5..9531f81 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -1,4 +1,4 @@ -"""Unit tests for plaud_tools.doctor. +"""Unit tests for plaud_tools.cli.doctor. Every section of the JSON is independently testable via monkeypatching. No network calls or real filesystem state is required. @@ -10,8 +10,8 @@ import sys from pathlib import Path -from plaud_tools import doctor as _doctor_mod -from plaud_tools.doctor import ( +from plaud_tools.cli import doctor as _doctor_mod +from plaud_tools.cli.doctor import ( _ai_clients_section, _executables_section, _ffmpeg_path, @@ -21,7 +21,7 @@ run_doctor, run_doctor_json, ) -from plaud_tools.session import PlaudSession, SessionStore +from plaud_tools.core.session import PlaudSession, SessionStore # --------------------------------------------------------------------------- # Helpers / shared fixtures @@ -218,7 +218,7 @@ def test_not_detected_when_no_config_files(self, monkeypatch, tmp_path): "claude-code": tmp_path / "nonexistent2.json", "codex": tmp_path / "nonexistent.toml", } - monkeypatch.setattr("plaud_tools.ai_clients._client_paths", lambda: fake_paths) + monkeypatch.setattr("plaud_tools.core.ai_clients._client_paths", lambda: fake_paths) monkeypatch.setattr(_doctor_mod, "_mcp_exe_path", lambda: tmp_path / "plaud-mcp.exe") section = _ai_clients_section() @@ -241,7 +241,7 @@ def test_connected_when_config_matches(self, monkeypatch, tmp_path): "claude-code": tmp_path / "nonexistent.json", "codex": tmp_path / "nonexistent.toml", } - monkeypatch.setattr("plaud_tools.ai_clients._client_paths", lambda: fake_paths) + monkeypatch.setattr("plaud_tools.core.ai_clients._client_paths", lambda: fake_paths) monkeypatch.setattr(_doctor_mod, "_mcp_exe_path", lambda: mcp_exe) section = _ai_clients_section() @@ -265,7 +265,7 @@ def test_stale_when_mcp_command_differs(self, monkeypatch, tmp_path): "claude-code": tmp_path / "nonexistent.json", "codex": tmp_path / "nonexistent.toml", } - monkeypatch.setattr("plaud_tools.ai_clients._client_paths", lambda: fake_paths) + monkeypatch.setattr("plaud_tools.core.ai_clients._client_paths", lambda: fake_paths) monkeypatch.setattr(_doctor_mod, "_mcp_exe_path", lambda: new_mcp) section = _ai_clients_section() @@ -299,7 +299,7 @@ def _stub_env(self, monkeypatch, tmp_path): "claude-code": tmp_path / "nonexistent2.json", "codex": tmp_path / "nonexistent.toml", } - monkeypatch.setattr("plaud_tools.ai_clients._client_paths", lambda: fake_client_paths) + monkeypatch.setattr("plaud_tools.core.ai_clients._client_paths", lambda: fake_client_paths) def test_schema_keys_present(self, monkeypatch, tmp_path): self._stub_env(monkeypatch, tmp_path) @@ -384,10 +384,10 @@ def _stub_env(self, monkeypatch, tmp_path): "claude-code": tmp_path / "nonexistent2.json", "codex": tmp_path / "nonexistent.toml", } - monkeypatch.setattr("plaud_tools.ai_clients._client_paths", lambda: fake_client_paths) + monkeypatch.setattr("plaud_tools.core.ai_clients._client_paths", lambda: fake_client_paths) def test_doctor_returns_valid_json(self, monkeypatch, tmp_path): - from plaud_tools.cli import run_cli + from plaud_tools.cli.cli import run_cli self._stub_env(monkeypatch, tmp_path) @@ -430,7 +430,7 @@ def _stub_env(self, monkeypatch, tmp_path): "claude-code": tmp_path / "nonexistent2.json", "codex": tmp_path / "nonexistent.toml", } - monkeypatch.setattr("plaud_tools.ai_clients._client_paths", lambda: fake_client_paths) + monkeypatch.setattr("plaud_tools.core.ai_clients._client_paths", lambda: fake_client_paths) def test_mcp_lifecycle_field_present(self, monkeypatch, tmp_path): self._stub_env(monkeypatch, tmp_path) @@ -457,7 +457,7 @@ def test_mcp_lifecycle_psutil_when_available(self, monkeypatch, tmp_path): fake_psutil = types.ModuleType("psutil") monkeypatch.setitem(sys.modules, "psutil", fake_psutil) - import plaud_tools.mcp_lifecycle as _lc + import plaud_tools.cli.process_probe as _lc monkeypatch.setattr(_lc, "active_enumerator_name", lambda: "psutil") monkeypatch.setattr(_doctor_mod, "active_enumerator_name", lambda: "psutil") diff --git a/tests/test_errors.py b/tests/test_errors.py index 42d2c50..a5dfd75 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -12,7 +12,7 @@ import json from urllib.error import HTTPError -from plaud_tools.errors import PlaudApiError +from plaud_tools.core.errors import PlaudApiError # --------------------------------------------------------------------------- # Helper: build a urllib.error.HTTPError with a readable body @@ -195,7 +195,7 @@ def test_returns_tuple(self): class TestOldSymbolsDeleted: def test_http_error_to_api_error_gone_from_transport(self): """_http_error_to_api_error must not be importable from transport.""" - import plaud_tools.transport as transport_mod + import plaud_tools.core.transport as transport_mod assert not hasattr(transport_mod, "_http_error_to_api_error"), ( "_http_error_to_api_error still exists in transport — delete it" @@ -203,7 +203,7 @@ def test_http_error_to_api_error_gone_from_transport(self): def test_classify_api_error_gone_from_mcp(self): """_classify_api_error must not be importable from mcp.""" - import plaud_tools.mcp as mcp_mod + import plaud_tools.mcp_pt.mcp as mcp_mod assert not hasattr(mcp_mod, "_classify_api_error"), ( "_classify_api_error still exists in mcp — delete it" diff --git a/tests/test_incremental_browse.py b/tests/test_incremental_browse.py index b9511c1..9209e77 100644 --- a/tests/test_incremental_browse.py +++ b/tests/test_incremental_browse.py @@ -14,8 +14,8 @@ from typing import Any from unittest.mock import MagicMock -from plaud_tools.models import Recording -from plaud_tools.query import BROWSE_PAGE_SIZE, collect_filtered_paged +from plaud_tools.core.models import Recording +from plaud_tools.core.query import BROWSE_PAGE_SIZE, collect_filtered_paged # --------------------------------------------------------------------------- # Helpers @@ -318,7 +318,7 @@ def list_recordings_stub(query=None): # type: ignore[override] return mock_client, calls def _build_handlers(self, mock_client: Any) -> Any: - from plaud_tools.mcp import build_handlers + from plaud_tools.mcp_pt.mcp import build_handlers return build_handlers(lambda: mock_client) @@ -493,7 +493,7 @@ def list_recordings_stub(query=None): # type: ignore[override] return mock_client, calls def test_list_with_query_filter_uses_paging(self): - from plaud_tools.cli import run_cli + from plaud_tools.cli.cli import run_cli recs = [make_rec(f"r{i}", filename="Meeting Note", start_time=i * 1000) for i in range(5)] client, call_log = self._make_client([recs]) @@ -504,7 +504,7 @@ def test_list_with_query_filter_uses_paging(self): assert len(call_log) >= 1 def test_search_uses_paging(self): - from plaud_tools.cli import run_cli + from plaud_tools.cli.cli import run_cli recs = [make_rec(f"r{i}", filename="Project X", start_time=i * 1000) for i in range(4)] client, call_log = self._make_client([recs]) @@ -515,7 +515,7 @@ def test_search_uses_paging(self): def test_list_without_filters_does_not_use_paging_path(self): # Without filters the CLI calls list_recordings with a limit query directly. - from plaud_tools.cli import run_cli + from plaud_tools.cli.cli import run_cli direct_recs = [make_rec(f"r{i}", start_time=i * 1000) for i in range(3)] mock_client = MagicMock() @@ -529,7 +529,7 @@ def test_list_without_filters_does_not_use_paging_path(self): assert query_arg.limit == 3 def test_list_sparse_matches_fetches_multiple_pages(self): - from plaud_tools.cli import run_cli + from plaud_tools.cli.cli import run_cli # Page 1: 200 records, 1 matches query; Page 2: 3 records, all match. def make_page1() -> list[Recording]: diff --git a/tests/test_interfaces.py b/tests/test_interfaces.py index 199e37c..e15e97b 100644 --- a/tests/test_interfaces.py +++ b/tests/test_interfaces.py @@ -6,11 +6,11 @@ import pytest -from plaud_tools.cli import main, run_cli -from plaud_tools.client import PlaudClient -from plaud_tools.mcp import build_handlers -from plaud_tools.models import FileTag, Recording, RecordingDetail -from plaud_tools.session import SessionStore +from plaud_tools.cli.cli import main, run_cli +from plaud_tools.core.client import PlaudClient +from plaud_tools.core.models import FileTag, Recording, RecordingDetail +from plaud_tools.core.session import SessionStore +from plaud_tools.mcp_pt.mcp import build_handlers class StubClient: @@ -58,7 +58,7 @@ def list_recordings(self, query=None): return list(self._ALL_RECORDINGS) def merge_recordings(self, ids: list[str], filename: str, **kwargs): - from plaud_tools.models import RecordingDetail + from plaud_tools.core.models import RecordingDetail return RecordingDetail( id="merged1", @@ -77,7 +77,7 @@ def merge_recordings(self, ids: list[str], filename: str, **kwargs): ) def upload_recording(self, data, filename, file_type, *, start_time=None, timezone_offset=None, **kwargs): - from plaud_tools.models import Recording + from plaud_tools.core.models import Recording return Recording( id="uploaded1", @@ -609,7 +609,7 @@ def login(self, email, password, region): def test_login_password_help_warns_about_exposure(): """--password help text must warn about process-listing exposure and point to safer alternatives.""" - from plaud_tools.cli import build_parser + from plaud_tools.cli.cli import build_parser parser = build_parser() # Drill into the login subparser to get its specific help @@ -641,7 +641,7 @@ def test_cli_login_uses_auth_and_returns_stored_shape(tmp_path: Path): def test_cli_main_prints_output_for_session_command(tmp_path: Path, capsys): - from plaud_tools import cli as cli_module + from plaud_tools.cli import cli as cli_module original_store = cli_module.SessionStore cli_module.SessionStore = lambda: SessionStore( @@ -665,7 +665,7 @@ def test_cli_main_returns_nonzero_on_missing_session(capsys, monkeypatch, tmp_pa monkeypatch.delenv("PLAUD_REGION", raising=False) monkeypatch.delenv("PLAUD_EMAIL", raising=False) monkeypatch.setattr( - "plaud_tools.cli.SessionStore", + "plaud_tools.cli.cli.SessionStore", lambda: SessionStore( tmp_path / "missing.json", service_name="plaud-tools-test-main", account_name="session" ), @@ -688,7 +688,7 @@ def _run_main_with_client(monkeypatch, client, argv): a specific client-raised error through main()'s except clauses, patch run_cli to forward the given client instead. """ - from plaud_tools import cli as cli_module + from plaud_tools.cli import cli as cli_module real_run_cli = cli_module.run_cli monkeypatch.setattr(cli_module, "run_cli", lambda argv_inner: real_run_cli(argv_inner, client=client)) @@ -696,7 +696,7 @@ def _run_main_with_client(monkeypatch, client, argv): def test_cli_main_session_expired_error_names_remedy(capsys, monkeypatch): - from plaud_tools.errors import PlaudSessionExpiredError + from plaud_tools.core.errors import PlaudSessionExpiredError class ExpiredClient: def list_recordings(self, query=None): @@ -711,7 +711,7 @@ def list_recordings(self, query=None): def test_cli_main_401_api_error_names_remedy(capsys, monkeypatch): - from plaud_tools.errors import PlaudApiError + from plaud_tools.core.errors import PlaudApiError class UnauthorizedClient: def list_recordings(self, query=None): @@ -724,7 +724,7 @@ def list_recordings(self, query=None): def test_cli_main_non_session_api_error_has_no_remedy_text(capsys, monkeypatch): - from plaud_tools.errors import PlaudApiError + from plaud_tools.core.errors import PlaudApiError class NotFoundClient: def list_recordings(self, query=None): @@ -803,7 +803,7 @@ def test_mcp_browse_recordings_next_after_set_when_full_page(): class ManyRecordingsClient(StubClient): def list_recordings(self, query=None): # Return enough items to fill a limit=1 page when filtered by folder - from plaud_tools.models import Recording + from plaud_tools.core.models import Recording return [ Recording( @@ -829,7 +829,7 @@ def test_mcp_browse_recordings_pagination_cursor_advances(): class ManyRecordingsClient(StubClient): def list_recordings(self, query=None): - from plaud_tools.models import Recording + from plaud_tools.core.models import Recording return [ Recording( @@ -1324,8 +1324,8 @@ def test_cli_dump_returns_raw_json(): def test_extract_inline_summary_handles_dict_data_content(): - from plaud_tools.client import PlaudClient - from plaud_tools.session import SessionManager, SessionStore + from plaud_tools.core.client import PlaudClient + from plaud_tools.core.session import SessionManager, SessionStore client = PlaudClient(SessionManager(SessionStore())) raw = { @@ -1339,8 +1339,8 @@ def test_extract_inline_summary_handles_dict_data_content(): def test_extract_inline_summary_fallback_by_data_type(): - from plaud_tools.client import PlaudClient - from plaud_tools.session import SessionManager, SessionStore + from plaud_tools.core.client import PlaudClient + from plaud_tools.core.session import SessionManager, SessionStore client = PlaudClient(SessionManager(SessionStore())) raw = { @@ -1359,9 +1359,9 @@ def test_extract_inline_summary_fallback_by_data_type(): def test_fetch_summary_from_data_link_handles_plain_text(): - from plaud_tools.client import PlaudClient - from plaud_tools.session import SessionManager, SessionStore - from plaud_tools.transport import HttpResponse + from plaud_tools.core.client import PlaudClient + from plaud_tools.core.session import SessionManager, SessionStore + from plaud_tools.core.transport import HttpResponse class PlainTextTransport: def request(self, method, url, headers, body=None, *, timeout=None): @@ -1640,7 +1640,7 @@ def test_mcp_upload_recording_folder_move_failure_is_partial_success(tmp_path): def test_cli_update_refuses_when_frozen(monkeypatch, capsys): import pytest - import plaud_tools.cli as cli_module + import plaud_tools.cli.cli as cli_module monkeypatch.setattr(cli_module.sys, "frozen", True, raising=False) # _handle_update never returns — it always calls sys.exit(), frozen or not. @@ -1656,7 +1656,7 @@ def test_cli_update_still_runs_pip_when_not_frozen(monkeypatch): import pytest - import plaud_tools.cli as cli_module + import plaud_tools.cli.cli as cli_module monkeypatch.delattr(cli_module.sys, "frozen", raising=False) @@ -1684,7 +1684,7 @@ def fake_run(cmd, **kwargs): def test_reconfigure_stdout_utf8_calls_reconfigure(monkeypatch): - import plaud_tools.cli as cli_module + import plaud_tools.cli.cli as cli_module calls = [] @@ -1698,7 +1698,7 @@ def reconfigure(self, **kwargs): def test_reconfigure_stdout_utf8_swallows_reconfigure_errors(monkeypatch): - import plaud_tools.cli as cli_module + import plaud_tools.cli.cli as cli_module class FakeStdout: def reconfigure(self, **kwargs): @@ -1709,7 +1709,7 @@ def reconfigure(self, **kwargs): def test_reconfigure_stdout_utf8_noop_when_unsupported(monkeypatch): - import plaud_tools.cli as cli_module + import plaud_tools.cli.cli as cli_module class FakeStdout: pass @@ -1725,7 +1725,7 @@ def test_main_transcript_non_ascii_survives_cp1252_stdout(monkeypatch, tmp_path) instead of the transcript actually reaching the caller.""" import io - import plaud_tools.cli as cli_module + import plaud_tools.cli.cli as cli_module class TranscriptClient(StubClient): def fetch_transcript(self, recording_id): diff --git a/tests/test_layout.py b/tests/test_layout.py index 4adb069..33eb6de 100644 --- a/tests/test_layout.py +++ b/tests/test_layout.py @@ -1,4 +1,4 @@ -"""Unit tests for plaud_tools.layout.InstallLayout.detect(). +"""Unit tests for plaud_tools.core.layout.InstallLayout.detect(). Covers all three channels (bundle, pip, dev) plus the critical regression test for the canonical-path autostart bug: a frozen-bundle install relocated @@ -14,7 +14,7 @@ import pytest -from plaud_tools.layout import InstallLayout +from plaud_tools.core.layout import InstallLayout # --------------------------------------------------------------------------- # Helpers diff --git a/tests/test_lifecycle_helpers.py b/tests/test_lifecycle_helpers.py index bc99efb..0069ae8 100644 --- a/tests/test_lifecycle_helpers.py +++ b/tests/test_lifecycle_helpers.py @@ -651,7 +651,7 @@ class TestRenderedPs1NoStrayPaths: OTHER_DIR = r"C:\Programs\OtherTool" def test_render_update_ps1_no_stray_install_dir(self): - from plaud_tools.ps1_templates import render_update_ps1 + from plaud_tools.tray.ps1_templates import render_update_ps1 result = render_update_ps1( tray_pid=1, @@ -662,7 +662,7 @@ def test_render_update_ps1_no_stray_install_dir(self): assert self.OTHER_DIR not in result def test_render_uninstall_ps1_no_stray_install_dir(self): - from plaud_tools.ps1_templates import render_uninstall_ps1 + from plaud_tools.tray.ps1_templates import render_uninstall_ps1 result = render_uninstall_ps1( tray_pid=1, @@ -671,7 +671,7 @@ def test_render_uninstall_ps1_no_stray_install_dir(self): assert self.OTHER_DIR not in result def test_render_update_ps1_contains_only_supplied_dir(self): - from plaud_tools.ps1_templates import render_update_ps1 + from plaud_tools.tray.ps1_templates import render_update_ps1 result = render_update_ps1( tray_pid=42, @@ -685,7 +685,7 @@ def test_render_update_ps1_contains_only_supplied_dir(self): assert r"C:\Temp\plaud_update_42.zip" in result def test_render_uninstall_ps1_contains_only_supplied_dir(self): - from plaud_tools.ps1_templates import render_uninstall_ps1 + from plaud_tools.tray.ps1_templates import render_uninstall_ps1 result = render_uninstall_ps1( tray_pid=99, @@ -702,7 +702,7 @@ def test_update_ps1_script_uses_install_dir_parameter_not_literals(self): The docstring may show examples like C:\\Programs\\PlaudTools, but the functional code must not reference any machine-specific install path. """ - from plaud_tools.ps1_templates import scripts_dir + from plaud_tools.tray.ps1_templates import scripts_dir content = (scripts_dir() / "update.ps1").read_text(encoding="utf-8") # The functional code must reference $InstallDir @@ -713,7 +713,7 @@ def test_update_ps1_script_uses_install_dir_parameter_not_literals(self): def test_uninstall_ps1_script_uses_install_dir_parameter_not_literals(self): """The raw uninstall.ps1 code sections use $InstallDir, not hard-coded paths.""" - from plaud_tools.ps1_templates import scripts_dir + from plaud_tools.tray.ps1_templates import scripts_dir content = (scripts_dir() / "uninstall.ps1").read_text(encoding="utf-8") assert "$InstallDir" in content @@ -730,7 +730,7 @@ class TestRenderUpdatePs1Snapshot: """Structural snapshot tests for the rendered update dispatcher.""" def _render(self, **kwargs): - from plaud_tools.ps1_templates import render_update_ps1 + from plaud_tools.tray.ps1_templates import render_update_ps1 defaults = dict( tray_pid=1234, @@ -777,7 +777,7 @@ class TestRenderUninstallPs1Snapshot: """Structural snapshot tests for the rendered uninstall dispatcher.""" def _render(self, **kwargs): - from plaud_tools.ps1_templates import render_uninstall_ps1 + from plaud_tools.tray.ps1_templates import render_uninstall_ps1 defaults = dict( tray_pid=999, diff --git a/tests/test_live_integration.py b/tests/test_live_integration.py index de467e4..8373bbe 100644 --- a/tests/test_live_integration.py +++ b/tests/test_live_integration.py @@ -4,8 +4,8 @@ import pytest -from plaud_tools.client import PlaudClient, PlaudRecordingQuery -from plaud_tools.session import FileSessionStore, SessionManager +from plaud_tools.core.client import PlaudClient, PlaudRecordingQuery +from plaud_tools.core.session import FileSessionStore, SessionManager pytestmark = [ pytest.mark.live, diff --git a/tests/test_login_window.py b/tests/test_login_window.py index 0f81f6d..9279ee5 100644 --- a/tests/test_login_window.py +++ b/tests/test_login_window.py @@ -24,7 +24,7 @@ import time from unittest.mock import MagicMock -from plaud_tools.errors import PlaudApiError +from plaud_tools.core.errors import PlaudApiError from plaud_tools.tray.setup import _widget_alive from plaud_tools.tray.windows import login as login_mod diff --git a/tests/test_mcp_error_codes.py b/tests/test_mcp_error_codes.py index 6ac4a19..eda0af9 100644 --- a/tests/test_mcp_error_codes.py +++ b/tests/test_mcp_error_codes.py @@ -8,8 +8,8 @@ import pytest -from plaud_tools.errors import PlaudApiError, PlaudSessionExpiredError -from plaud_tools.mcp import ( +from plaud_tools.core.errors import PlaudApiError, PlaudSessionExpiredError +from plaud_tools.mcp_pt.mcp import ( _diagnose_session_state, _error_result, _write_event, @@ -223,7 +223,7 @@ def test_401_api_error_emits_session_expired_event(self, tmp_path, monkeypatch): PlaudSessionExpiredError branch did, so the tray toast never appeared for a mid-session 401 even though it was classified session_expired.""" events_file = tmp_path / "events.jsonl" - monkeypatch.setattr("plaud_tools.mcp._events_path", lambda: events_file) + monkeypatch.setattr("plaud_tools.mcp_pt.mcp._events_path", lambda: events_file) handlers = _make_handlers(PlaudApiError("unauthorized", http_status=401)) handlers["browse_recordings"]() @@ -353,7 +353,7 @@ def test_invalid_wait_mode_returns_validation_code(self): class TestWriteEvent: def test_writes_session_expired_event(self, tmp_path, monkeypatch): events_file = tmp_path / "events.jsonl" - monkeypatch.setattr("plaud_tools.mcp._events_path", lambda: events_file) + monkeypatch.setattr("plaud_tools.mcp_pt.mcp._events_path", lambda: events_file) _write_event("session_expired", reason="token_expired") @@ -368,7 +368,7 @@ def test_writes_session_expired_event(self, tmp_path, monkeypatch): def test_write_event_never_raises(self, tmp_path, monkeypatch): """_write_event must not propagate even when the path is unwritable.""" monkeypatch.setattr( - "plaud_tools.mcp._events_path", + "plaud_tools.mcp_pt.mcp._events_path", lambda: Path("/this/does/not/exist/events.jsonl"), ) # Should not raise @@ -377,7 +377,7 @@ def test_write_event_never_raises(self, tmp_path, monkeypatch): def test_session_expired_via_handler_writes_event(self, tmp_path, monkeypatch): """Hitting PlaudSessionExpiredError through _call writes an event.""" events_file = tmp_path / "events.jsonl" - monkeypatch.setattr("plaud_tools.mcp._events_path", lambda: events_file) + monkeypatch.setattr("plaud_tools.mcp_pt.mcp._events_path", lambda: events_file) mock_client = MagicMock() mock_client.list_recordings.side_effect = PlaudSessionExpiredError("expired") @@ -400,7 +400,7 @@ def test_session_expired_via_handler_writes_event(self, tmp_path, monkeypatch): def test_no_session_event_includes_diagnostic(self, tmp_path, monkeypatch): events_file = tmp_path / "events.jsonl" - monkeypatch.setattr("plaud_tools.mcp._events_path", lambda: events_file) + monkeypatch.setattr("plaud_tools.mcp_pt.mcp._events_path", lambda: events_file) handlers = build_handlers(lambda: None) handlers["browse_recordings"]() record = json.loads(events_file.read_text(encoding="utf-8").splitlines()[0]) @@ -447,13 +447,13 @@ def _b64u(s: str) -> str: return _b64.urlsafe_b64encode(s.encode()).decode().rstrip("=") fake_jwt = ".".join([_b64u('{"alg":"HS256","typ":"UT"}'), _b64u('{"exp":9999999999}'), "sig"]) - from plaud_tools.session import FileSessionStore, PlaudSession, SessionStore + from plaud_tools.core.session import FileSessionStore, PlaudSession, SessionStore session_path = tmp_path / "session.json" FileSessionStore(session_path).save(PlaudSession(access_token=fake_jwt, region="us")) # Force the diagnostic to use the file store by making keyring unavailable. - import plaud_tools.mcp as mcp_mod + import plaud_tools.mcp_pt.mcp as mcp_mod class _StoreFromTmp(SessionStore): def __init__(self): @@ -588,7 +588,7 @@ def test_rotation_occurs_when_file_exceeds_cap(self, tmp_path, monkeypatch): - events.jsonl exists and contains exactly the new event - no exception escaped _write_event """ - from plaud_tools import mcp as mcp_mod + from plaud_tools.mcp_pt import mcp as mcp_mod events_file = tmp_path / "events.jsonl" monkeypatch.setattr(mcp_mod, "_events_path", lambda: events_file) @@ -620,7 +620,7 @@ def test_rotation_replaces_existing_dot_one(self, tmp_path, monkeypatch): present; os.replace must be used instead. This test confirms no exception escapes and the stale .1 is replaced. """ - from plaud_tools import mcp as mcp_mod + from plaud_tools.mcp_pt import mcp as mcp_mod events_file = tmp_path / "events.jsonl" rotated = tmp_path / "events.jsonl.1" @@ -652,8 +652,8 @@ def test_failed_rotation_does_not_raise(self, tmp_path, monkeypatch): even if the rotation fails the append can still succeed. But the key guarantee — no exception escapes — must hold regardless. """ - import plaud_tools.mcp as _mcp_module - from plaud_tools import mcp as mcp_mod + import plaud_tools.mcp_pt.mcp as _mcp_module + from plaud_tools.mcp_pt import mcp as mcp_mod events_file = tmp_path / "events.jsonl" monkeypatch.setattr(mcp_mod, "_events_path", lambda: events_file) @@ -671,7 +671,7 @@ def _boom(*args, **kwargs): def test_no_rotation_below_cap(self, tmp_path, monkeypatch): """Files smaller than _EVENTS_MAX_BYTES must not be rotated.""" - from plaud_tools import mcp as mcp_mod + from plaud_tools.mcp_pt import mcp as mcp_mod events_file = tmp_path / "events.jsonl" monkeypatch.setattr(mcp_mod, "_events_path", lambda: events_file) diff --git a/tests/test_mcp_golden.py b/tests/test_mcp_golden.py index 8bf24fc..e1fc644 100644 --- a/tests/test_mcp_golden.py +++ b/tests/test_mcp_golden.py @@ -34,7 +34,7 @@ import pytest -from plaud_tools.server import _TOOLS +from plaud_tools.mcp_pt.server import _TOOLS _GOLDEN_PATH = Path(__file__).parent / "data" / "tool_descriptions.golden.json" diff --git a/tests/test_mcp_wave4_surface.py b/tests/test_mcp_wave4_surface.py index 617dc7c..cf7daef 100644 --- a/tests/test_mcp_wave4_surface.py +++ b/tests/test_mcp_wave4_surface.py @@ -17,9 +17,9 @@ import json from unittest.mock import MagicMock -from plaud_tools.errors import PlaudApiError -from plaud_tools.mcp import _WAIT_TIMEOUT_S, build_handlers -from plaud_tools.models import RecordingDetail +from plaud_tools.core.errors import PlaudApiError +from plaud_tools.core.models import RecordingDetail +from plaud_tools.mcp_pt.mcp import _WAIT_TIMEOUT_S, build_handlers # --------------------------------------------------------------------------- # edit_transcript — merged rename_speaker + correct_transcript @@ -334,7 +334,7 @@ def test_wait_timeout_returns_still_processing(self): class TestCompactJson: def test_json_result_has_no_extra_whitespace(self): - from plaud_tools.mcp import _json_result + from plaud_tools.mcp_pt.mcp import _json_result result = _json_result({"a": 1, "b": [1, 2, 3]}) text = result["content"][0]["text"] diff --git a/tests/test_ps1_templates.py b/tests/test_ps1_templates.py index 7abea8b..49f9d26 100644 --- a/tests/test_ps1_templates.py +++ b/tests/test_ps1_templates.py @@ -9,7 +9,7 @@ import pytest -from plaud_tools.ps1_templates import ( +from plaud_tools.tray.ps1_templates import ( _ps_escape, render_uninstall_ps1, render_update_ps1, diff --git a/tests/test_query.py b/tests/test_query.py index f8d2edb..5ad8101 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -13,8 +13,8 @@ import pytest -from plaud_tools.models import Recording -from plaud_tools.query import filter_recordings, parse_isoish, summarize_recording +from plaud_tools.core.models import Recording +from plaud_tools.core.query import filter_recordings, parse_isoish, summarize_recording # --------------------------------------------------------------------------- # Helpers diff --git a/tests/test_server.py b/tests/test_server.py index 1de2855..5fcdb57 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -10,7 +10,7 @@ import mcp.types as mcp_types -from plaud_tools.server import _TOOLS, _make_server, _mcp_log_path, _setup_mcp_logging +from plaud_tools.mcp_pt.server import _TOOLS, _make_server, _mcp_log_path, _setup_mcp_logging _EXPECTED_TOOL_NAMES = { "browse_recordings", @@ -216,7 +216,7 @@ def test_handler_error_result_propagates_is_error_true(self, monkeypatch): reports "no session" so this cannot accidentally hit a real Plaud session/network on the machine running the tests. """ - from plaud_tools import server as srv_mod + from plaud_tools.mcp_pt import server as srv_mod monkeypatch.setattr(srv_mod, "SessionStore", lambda: _NoSessionStore()) server = srv_mod._make_server() @@ -229,7 +229,7 @@ def test_delete_recording_refused_confirm_returns_is_error_true(self, monkeypatc """A validation-refused delete (confirm=false) must set isError=True on the CallToolResult, independent of whether a session even exists — the confirm gate is checked before any client call.""" - from plaud_tools import server as srv_mod + from plaud_tools.mcp_pt import server as srv_mod monkeypatch.setattr(srv_mod, "SessionStore", lambda: _NoSessionStore()) server = srv_mod._make_server() @@ -334,7 +334,7 @@ def test_make_server_constructs_one_session_manager(monkeypatch): The fix hoists construction into ``_make_server`` itself, so the count is exactly one immediately after the function returns. """ - from plaud_tools import server as srv_mod + from plaud_tools.mcp_pt import server as srv_mod instances: list[object] = [] original_init = srv_mod.SessionManager.__init__ diff --git a/tests/test_session_cache_freshness.py b/tests/test_session_cache_freshness.py index 8293ab3..06cc0a6 100644 --- a/tests/test_session_cache_freshness.py +++ b/tests/test_session_cache_freshness.py @@ -26,7 +26,7 @@ import pytest -from plaud_tools.session import ( +from plaud_tools.core.session import ( FileSessionStore, PlaudSession, SessionManager, @@ -50,7 +50,7 @@ def _keyring_patch(monkeypatch: pytest.MonkeyPatch, kr: Any) -> None: """Patch importlib so that ``import keyring`` returns *kr*.""" real_import = __import__ monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda n: kr if n == "keyring" else real_import(n), ) @@ -180,7 +180,7 @@ def test_cross_instance_pickup_no_extra_keyring_reads( ) # Disable keyring so load() falls through to file_store. monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: None if name == "keyring" else __import__(name), ) store1.file_store.save(PlaudSession(access_token=jwt_v1, region="us", email="v1@example.com")) @@ -278,7 +278,7 @@ def test_reload_after_ttl_expires(self, monkeypatch: pytest.MonkeyPatch) -> None # Fast-forward time past the TTL by patching `time()` inside session.py. original_time = _time.time() monkeypatch.setattr( - "plaud_tools.session.time", + "plaud_tools.core.session.time", lambda: original_time + SessionManager._CACHE_TTL_SECONDS + 1.0, ) @@ -310,7 +310,7 @@ def test_cross_instance_ttl_pickup(self, monkeypatch: pytest.MonkeyPatch) -> Non # Fast-forward past TTL. original_time = _time.time() monkeypatch.setattr( - "plaud_tools.session.time", + "plaud_tools.core.session.time", lambda: original_time + SessionManager._CACHE_TTL_SECONDS + 1.0, ) diff --git a/tests/test_session_diagnose.py b/tests/test_session_diagnose.py index 9190612..3bc33b8 100644 --- a/tests/test_session_diagnose.py +++ b/tests/test_session_diagnose.py @@ -17,7 +17,7 @@ import json import time as _time -from plaud_tools.session import ( +from plaud_tools.core.session import ( FileSessionStore, PlaudSession, SessionManager, @@ -69,7 +69,7 @@ def test_returns_missing_source_when_no_session(self, tmp_path, monkeypatch): ) # Disable keyring so the file-store is the last resort monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: None if name == "keyring" else __import__(name), ) manager = SessionManager(store) @@ -87,7 +87,7 @@ def test_diagnose_error_field_absent_on_clean_missing(self, tmp_path, monkeypatc dpapi_path=None, ) monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: None if name == "keyring" else __import__(name), ) manager = SessionManager(store) @@ -176,7 +176,7 @@ def test_malformed_jwt_omits_token_typ(self, tmp_path, monkeypatch): dpapi_path=None, ) monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: None if name == "keyring" else __import__(name), ) manager = SessionManager(store) @@ -203,7 +203,7 @@ def test_missing_exp_claim_omits_days_until_expiry(self, tmp_path, monkeypatch): dpapi_path=None, ) monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: None if name == "keyring" else __import__(name), ) manager = SessionManager(store) @@ -240,7 +240,7 @@ def get_credential(*_a, **_k): return None monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: FakeKeyring if name == "keyring" else __import__(name), ) monkeypatch.delenv("PLAUD_ACCESS_TOKEN", raising=False) @@ -282,7 +282,7 @@ def test_file_source_returns_correct_store_source(self, tmp_path, monkeypatch): dpapi_path=None, ) monkeypatch.setattr( - "plaud_tools.session.importlib.import_module", + "plaud_tools.core.session.importlib.import_module", lambda name: None if name == "keyring" else __import__(name), ) manager = SessionManager(store) @@ -353,9 +353,9 @@ def _b64u(s: str) -> str: monkeypatch.setenv("PLAUD_ACCESS_TOKEN", fake_jwt) events_file = tmp_path / "events.jsonl" - monkeypatch.setattr("plaud_tools.mcp._events_path", lambda: events_file) + monkeypatch.setattr("plaud_tools.mcp_pt.mcp._events_path", lambda: events_file) - from plaud_tools.mcp import _emit_session_expired + from plaud_tools.mcp_pt.mcp import _emit_session_expired _emit_session_expired("token_expired") diff --git a/tests/test_session_hardening.py b/tests/test_session_hardening.py index 20703e8..feff0b1 100644 --- a/tests/test_session_hardening.py +++ b/tests/test_session_hardening.py @@ -9,7 +9,7 @@ import pytest -from plaud_tools.session import FileSessionStore, PlaudSession, SessionStore +from plaud_tools.core.session import FileSessionStore, PlaudSession, SessionStore # --------------------------------------------------------------------------- # #145 — FileSessionStore.load() must tolerate BOM / corrupt / partial JSON @@ -113,7 +113,7 @@ def test_save_uses_os_replace(self, tmp_path: Path, monkeypatch: pytest.MonkeyPa """Pin the mechanism explicitly: save() must go through os.replace (atomic on both POSIX and Windows), not Path.rename or a direct write to the final path.""" - import plaud_tools.session as session_mod + import plaud_tools.core.session as session_mod path = tmp_path / "session.json" store = FileSessionStore(path) @@ -194,7 +194,7 @@ def test_session_store_clear_also_removes_legacy_file( ) -> None: """Same guarantee through the full SessionStore.clear() (keyring + dpapi + file), not just FileSessionStore.clear() directly.""" - from plaud_tools.session import PlaudSession as _PlaudSession + from plaud_tools.core.session import PlaudSession as _PlaudSession legacy_dir = tmp_path / "legacy" / ".config" / "plaud-tools" legacy_dir.mkdir(parents=True) diff --git a/tests/test_session_token_buffer.py b/tests/test_session_token_buffer.py index 2ef6a8e..5a35470 100644 --- a/tests/test_session_token_buffer.py +++ b/tests/test_session_token_buffer.py @@ -30,8 +30,8 @@ import pytest -from plaud_tools.errors import PlaudSessionExpiredError -from plaud_tools.session import ( +from plaud_tools.core.errors import PlaudSessionExpiredError +from plaud_tools.core.session import ( TOKEN_REFRESH_BUFFER_SECONDS, TRAY_EXPIRY_WARNING_DAYS, FileSessionStore, @@ -152,7 +152,7 @@ def test_require_raises_once_cached_token_crosses_the_buffer( # would have kept serving the cached (now-refusable) session. real_time = _time.time monkeypatch.setattr( - "plaud_tools.session.time", + "plaud_tools.core.session.time", lambda: real_time() + TOKEN_REFRESH_BUFFER_SECONDS + 120, ) diff --git a/tests/test_transport.py b/tests/test_transport.py index 11fd23e..4e60010 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -9,8 +9,8 @@ import pytest -from plaud_tools.errors import PlaudApiError -from plaud_tools.transport import _DEFAULT_TIMEOUT, UrllibTransport +from plaud_tools.core.errors import PlaudApiError +from plaud_tools.core.transport import _DEFAULT_TIMEOUT, UrllibTransport # --------------------------------------------------------------------------- # Helper: build a urllib.error.HTTPError with a readable body @@ -118,7 +118,7 @@ def test_http_error_surfaces_structured_fields(self): http_exc = _make_http_error(429, body) transport = UrllibTransport() - with patch("plaud_tools.transport.urlopen", side_effect=http_exc): + with patch("plaud_tools.core.transport.urlopen", side_effect=http_exc): with pytest.raises(PlaudApiError) as exc_info: transport.request("GET", "https://example.com/api", {}) @@ -131,7 +131,7 @@ def test_url_error_raises_plain_plaud_api_error(self): url_exc = URLError("Connection refused") transport = UrllibTransport() - with patch("plaud_tools.transport.urlopen", side_effect=url_exc): + with patch("plaud_tools.core.transport.urlopen", side_effect=url_exc): with pytest.raises(PlaudApiError) as exc_info: transport.request("GET", "https://example.com/api", {}) @@ -169,7 +169,9 @@ def test_constructor_timeout_is_forwarded_to_urlopen(self): """A custom constructor timeout must arrive at urlopen unchanged.""" transport = UrllibTransport(timeout=45.0) - with patch("plaud_tools.transport.urlopen", return_value=_make_successful_response()) as mock_open: + with patch( + "plaud_tools.core.transport.urlopen", return_value=_make_successful_response() + ) as mock_open: transport.request("GET", "https://example.com/api", {}) _req, kwargs_or_pos = mock_open.call_args[0], mock_open.call_args # noqa: F841 # destructuring for clarity @@ -180,7 +182,9 @@ def test_per_call_timeout_overrides_constructor(self): """A per-call timeout= kwarg must take precedence over the constructor default.""" transport = UrllibTransport(timeout=5.0) - with patch("plaud_tools.transport.urlopen", return_value=_make_successful_response()) as mock_open: + with patch( + "plaud_tools.core.transport.urlopen", return_value=_make_successful_response() + ) as mock_open: transport.request("GET", "https://example.com/api", {}, timeout=99.0) args = mock_open.call_args @@ -191,7 +195,9 @@ def test_default_30s_reaches_urlopen_without_override(self): """With no constructor or call-site override, urlopen sees 30 s.""" transport = UrllibTransport() - with patch("plaud_tools.transport.urlopen", return_value=_make_successful_response()) as mock_open: + with patch( + "plaud_tools.core.transport.urlopen", return_value=_make_successful_response() + ) as mock_open: transport.request("PUT", "https://s3.example.com/upload", {}, b"data") args = mock_open.call_args @@ -206,7 +212,7 @@ def test_timeout_error_raises_plaud_api_error(self): """Built-in TimeoutError (not a URLError subclass) must become PlaudApiError.""" transport = UrllibTransport(timeout=30.0) - with patch("plaud_tools.transport.urlopen", side_effect=TimeoutError("timed out")): + with patch("plaud_tools.core.transport.urlopen", side_effect=TimeoutError("timed out")): with pytest.raises(PlaudApiError) as exc_info: transport.request("GET", "https://example.com/api", {}) @@ -218,7 +224,7 @@ def test_socket_timeout_raises_plaud_api_error(self): """socket.timeout (which may surface directly from urlopen) must become PlaudApiError.""" transport = UrllibTransport(timeout=30.0) - with patch("plaud_tools.transport.urlopen", side_effect=TimeoutError("socket timed out")): + with patch("plaud_tools.core.transport.urlopen", side_effect=TimeoutError("socket timed out")): with pytest.raises(PlaudApiError) as exc_info: transport.request("GET", "https://example.com/api", {}) @@ -232,7 +238,7 @@ def test_url_error_with_socket_timeout_reason_raises_plaud_api_error(self): cause = TimeoutError("read timed out") url_exc = URLError(reason=cause) - with patch("plaud_tools.transport.urlopen", side_effect=url_exc): + with patch("plaud_tools.core.transport.urlopen", side_effect=url_exc): with pytest.raises(PlaudApiError) as exc_info: transport.request("GET", "https://example.com/api", {}) @@ -243,7 +249,7 @@ def test_timeout_message_includes_budget(self): """Error message must include the effective timeout value for observability.""" transport = UrllibTransport(timeout=120.0) - with patch("plaud_tools.transport.urlopen", side_effect=TimeoutError()): + with patch("plaud_tools.core.transport.urlopen", side_effect=TimeoutError()): with pytest.raises(PlaudApiError) as exc_info: transport.request("GET", "https://example.com/api", {}) @@ -255,7 +261,7 @@ def test_timeout_error_flags_network_error_and_classifies_transient(self): api_error — a 30s blip must not abort a multi-minute wait.""" transport = UrllibTransport(timeout=30.0) - with patch("plaud_tools.transport.urlopen", side_effect=TimeoutError("timed out")): + with patch("plaud_tools.core.transport.urlopen", side_effect=TimeoutError("timed out")): with pytest.raises(PlaudApiError) as exc_info: transport.request("GET", "https://example.com/api", {}) @@ -270,7 +276,7 @@ def test_url_error_flags_network_error_and_classifies_transient(self): must also be flagged as a retryable transient.""" transport = UrllibTransport() - with patch("plaud_tools.transport.urlopen", side_effect=URLError("Connection refused")): + with patch("plaud_tools.core.transport.urlopen", side_effect=URLError("Connection refused")): with pytest.raises(PlaudApiError) as exc_info: transport.request("GET", "https://example.com/api", {}) @@ -288,7 +294,7 @@ def test_http_error_does_not_set_network_error_flag(self): http_exc = _make_http_error(500, body) transport = UrllibTransport() - with patch("plaud_tools.transport.urlopen", side_effect=http_exc): + with patch("plaud_tools.core.transport.urlopen", side_effect=http_exc): with pytest.raises(PlaudApiError) as exc_info: transport.request("GET", "https://example.com/api", {}) diff --git a/tests/test_tray_correctness.py b/tests/test_tray_correctness.py index e0535c3..740d561 100644 --- a/tests/test_tray_correctness.py +++ b/tests/test_tray_correctness.py @@ -201,7 +201,7 @@ def test_test_connection_succeeds_when_api_responds(): def test_test_connection_reports_api_error(): - from plaud_tools.errors import PlaudApiError + from plaud_tools.core.errors import PlaudApiError results = _run_test_connection(_FakePlaudClient(raises=PlaudApiError("bad auth"))) assert len(results) == 1 @@ -251,13 +251,13 @@ def test_mcp_upload_recording_catches_ffmpeg_runtime_error(tmp_path, monkeypatch not the file's own get_file_type/transcode split) so this pins actual behaviour rather than a hand-rolled reimplementation of the handler. """ - from plaud_tools.mcp import build_handlers + from plaud_tools.mcp_pt.mcp import build_handlers # .wav needs transcoding (unlike .mp3, which is uploaded natively). wav_file = tmp_path / "audio.wav" wav_file.write_bytes(b"fake wav") - import plaud_tools.transcode as transcode_module + import plaud_tools.core.transcode as transcode_module monkeypatch.setattr( transcode_module, @@ -276,8 +276,8 @@ def test_mcp_upload_recording_catches_ffmpeg_runtime_error(tmp_path, monkeypatch def test_mcp_call_still_catches_plaud_api_error(): """_call must still catch PlaudApiError and return an MCP error dict.""" - from plaud_tools.errors import PlaudApiError - from plaud_tools.mcp import _call + from plaud_tools.core.errors import PlaudApiError + from plaud_tools.mcp_pt.mcp import _call def get_client(): return MagicMock() @@ -293,7 +293,7 @@ def fn(client): def test_mcp_call_still_catches_value_error(): """_call must still catch ValueError and return an MCP error dict.""" - from plaud_tools.mcp import _call + from plaud_tools.mcp_pt.mcp import _call def get_client(): return MagicMock() diff --git a/tests/test_tray_expiry_ux.py b/tests/test_tray_expiry_ux.py index c2fd96c..17e782d 100644 --- a/tests/test_tray_expiry_ux.py +++ b/tests/test_tray_expiry_ux.py @@ -19,7 +19,7 @@ from unittest.mock import MagicMock -from plaud_tools.session import TRAY_EXPIRY_WARNING_DAYS +from plaud_tools.core.session import TRAY_EXPIRY_WARNING_DAYS from plaud_tools.tray.app import TrayApp diff --git a/tests/test_upload.py b/tests/test_upload.py index 3d6b9dd..c2f5f9c 100644 --- a/tests/test_upload.py +++ b/tests/test_upload.py @@ -7,11 +7,11 @@ import pytest -from plaud_tools.client import _CHUNK_SIZE, PlaudClient -from plaud_tools.errors import PlaudApiError -from plaud_tools.session import FileSessionStore, PlaudSession, SessionManager -from plaud_tools.transcode import get_file_type, transcode_to_mp3_path -from plaud_tools.transport import HttpResponse +from plaud_tools.core.client import _CHUNK_SIZE, PlaudClient +from plaud_tools.core.errors import PlaudApiError +from plaud_tools.core.session import FileSessionStore, PlaudSession, SessionManager +from plaud_tools.core.transcode import get_file_type, transcode_to_mp3_path +from plaud_tools.core.transport import HttpResponse # --------------------------------------------------------------------------- # Helpers shared with test_client.py @@ -611,7 +611,7 @@ def test_wait_for_transcription_times_out(tmp_path): def test_transcode_raises_when_ffmpeg_not_found(tmp_path, monkeypatch): monkeypatch.delenv("FFMPEG_BIN", raising=False) - monkeypatch.setattr("plaud_tools.transcode.shutil.which", lambda _: None) + monkeypatch.setattr("plaud_tools.core.transcode.shutil.which", lambda _: None) src = tmp_path / "audio.wav" src.write_bytes(b"x") with pytest.raises(RuntimeError, match="Could not locate ffmpeg"): @@ -620,7 +620,7 @@ def test_transcode_raises_when_ffmpeg_not_found(tmp_path, monkeypatch): def test_find_ffmpeg_frozen_sibling(tmp_path, monkeypatch): """Frozen mode: ffmpeg.exe beside the exe is found first (MCP path).""" - from plaud_tools.transcode import _find_ffmpeg + from plaud_tools.core.transcode import _find_ffmpeg exe_dir = tmp_path / "mcp" exe_dir.mkdir() @@ -630,16 +630,16 @@ def test_find_ffmpeg_frozen_sibling(tmp_path, monkeypatch): monkeypatch.delenv("FFMPEG_BIN", raising=False) monkeypatch.setattr( - "plaud_tools.transcode.sys", type("S", (), {"frozen": True, "executable": str(fake_exe)})() + "plaud_tools.core.transcode.sys", type("S", (), {"frozen": True, "executable": str(fake_exe)})() ) - monkeypatch.setattr("plaud_tools.transcode.shutil.which", lambda _: None) + monkeypatch.setattr("plaud_tools.core.transcode.shutil.which", lambda _: None) assert _find_ffmpeg() == str(ffmpeg_exe) def test_find_ffmpeg_frozen_cli_falls_back_to_mcp_sibling(tmp_path, monkeypatch): """Frozen CLI mode: no ffmpeg beside the CLI exe, resolves ../mcp/ffmpeg.exe.""" - from plaud_tools.transcode import _find_ffmpeg + from plaud_tools.core.transcode import _find_ffmpeg # Lay out PlaudTools/cli/ and PlaudTools/mcp/ under tmp_path cli_dir = tmp_path / "cli" @@ -652,16 +652,16 @@ def test_find_ffmpeg_frozen_cli_falls_back_to_mcp_sibling(tmp_path, monkeypatch) monkeypatch.delenv("FFMPEG_BIN", raising=False) monkeypatch.setattr( - "plaud_tools.transcode.sys", type("S", (), {"frozen": True, "executable": str(fake_cli_exe)})() + "plaud_tools.core.transcode.sys", type("S", (), {"frozen": True, "executable": str(fake_cli_exe)})() ) - monkeypatch.setattr("plaud_tools.transcode.shutil.which", lambda _: None) + monkeypatch.setattr("plaud_tools.core.transcode.shutil.which", lambda _: None) assert _find_ffmpeg() == str(ffmpeg_exe) def test_find_ffmpeg_frozen_no_bundle_falls_back_to_path(tmp_path, monkeypatch): """Frozen mode: no bundled ffmpeg at all → falls back to shutil.which.""" - from plaud_tools.transcode import _find_ffmpeg + from plaud_tools.core.transcode import _find_ffmpeg cli_dir = tmp_path / "cli" cli_dir.mkdir() @@ -669,9 +669,9 @@ def test_find_ffmpeg_frozen_no_bundle_falls_back_to_path(tmp_path, monkeypatch): monkeypatch.delenv("FFMPEG_BIN", raising=False) monkeypatch.setattr( - "plaud_tools.transcode.sys", type("S", (), {"frozen": True, "executable": str(fake_cli_exe)})() + "plaud_tools.core.transcode.sys", type("S", (), {"frozen": True, "executable": str(fake_cli_exe)})() ) - monkeypatch.setattr("plaud_tools.transcode.shutil.which", lambda _: "/usr/bin/ffmpeg") + monkeypatch.setattr("plaud_tools.core.transcode.shutil.which", lambda _: "/usr/bin/ffmpeg") assert _find_ffmpeg() == "/usr/bin/ffmpeg" @@ -820,7 +820,7 @@ def fake_run(cmd, capture_output): Path(cmd[-1]).write_bytes(expected_output) return type("R", (), {"returncode": 0, "stderr": b""})() - monkeypatch.setattr("plaud_tools.transcode.subprocess.run", fake_run) + monkeypatch.setattr("plaud_tools.core.transcode.subprocess.run", fake_run) src = tmp_path / "audio.m4a" src.write_bytes(b"source audio") @@ -840,7 +840,7 @@ def test_transcode_to_mp3_path_raises_on_ffmpeg_failure(tmp_path, monkeypatch): def fake_run(cmd, capture_output): return type("R", (), {"returncode": 1, "stderr": b"bad input"})() - monkeypatch.setattr("plaud_tools.transcode.subprocess.run", fake_run) + monkeypatch.setattr("plaud_tools.core.transcode.subprocess.run", fake_run) src = tmp_path / "audio.m4a" src.write_bytes(b"bad audio") @@ -867,7 +867,7 @@ def __init__(self, *, folder_move_error: Exception | None = None): def upload_recording( self, data, filename, file_type, *, start_time=None, timezone_offset=None, timeout_s=None ): - from plaud_tools.models import Recording + from plaud_tools.core.models import Recording self._next_id += 1 # Record whether *data* was a real on-disk path so tests can assert @@ -882,7 +882,7 @@ def set_recording_folder(self, recording_id, folder_id): def test_upload_with_transcode_native_format_skips_ffmpeg(tmp_path): - from plaud_tools.transcode import upload_with_transcode + from plaud_tools.core.transcode import upload_with_transcode audio = tmp_path / "audio.mp3" audio.write_bytes(b"fake mp3") @@ -899,7 +899,7 @@ def test_upload_with_transcode_native_format_skips_ffmpeg(tmp_path): def test_upload_with_transcode_transcodes_non_native_format(tmp_path, monkeypatch): - from plaud_tools.transcode import upload_with_transcode + from plaud_tools.core.transcode import upload_with_transcode fake_ff = tmp_path / "ffmpeg" fake_ff.write_bytes(b"") @@ -913,7 +913,7 @@ def fake_run(cmd, capture_output): written_mp3_paths.append(out_path) return type("R", (), {"returncode": 0, "stderr": b""})() - monkeypatch.setattr("plaud_tools.transcode.subprocess.run", fake_run) + monkeypatch.setattr("plaud_tools.core.transcode.subprocess.run", fake_run) audio = tmp_path / "audio.wav" audio.write_bytes(b"raw wav bytes") @@ -930,7 +930,7 @@ def fake_run(cmd, capture_output): def test_upload_with_transcode_missing_file_raises_value_error(tmp_path): - from plaud_tools.transcode import upload_with_transcode + from plaud_tools.core.transcode import upload_with_transcode client = _FakeUploadClient() with pytest.raises(ValueError, match="file not found"): @@ -939,7 +939,7 @@ def test_upload_with_transcode_missing_file_raises_value_error(tmp_path): def test_upload_with_transcode_ffmpeg_failure_propagates_and_skips_upload(tmp_path, monkeypatch): - from plaud_tools.transcode import upload_with_transcode + from plaud_tools.core.transcode import upload_with_transcode fake_ff = tmp_path / "ffmpeg" fake_ff.write_bytes(b"") @@ -948,7 +948,7 @@ def test_upload_with_transcode_ffmpeg_failure_propagates_and_skips_upload(tmp_pa def fake_run(cmd, capture_output): return type("R", (), {"returncode": 1, "stderr": b"bad input"})() - monkeypatch.setattr("plaud_tools.transcode.subprocess.run", fake_run) + monkeypatch.setattr("plaud_tools.core.transcode.subprocess.run", fake_run) audio = tmp_path / "audio.wav" audio.write_bytes(b"raw wav bytes") @@ -960,7 +960,7 @@ def fake_run(cmd, capture_output): def test_upload_with_transcode_folder_move_success(tmp_path): - from plaud_tools.transcode import upload_with_transcode + from plaud_tools.core.transcode import upload_with_transcode audio = tmp_path / "audio.mp3" audio.write_bytes(b"fake mp3") @@ -977,8 +977,8 @@ def test_upload_with_transcode_folder_move_failure_is_partial_success(tmp_path): """#149: a post-upload folder-move failure must not lose the recording — the upload already succeeded, so the caller needs the id back to avoid re-uploading a duplicate.""" - from plaud_tools.errors import PlaudApiError - from plaud_tools.transcode import upload_with_transcode + from plaud_tools.core.errors import PlaudApiError + from plaud_tools.core.transcode import upload_with_transcode audio = tmp_path / "audio.mp3" audio.write_bytes(b"fake mp3") @@ -1005,14 +1005,14 @@ def test_upload_with_transcode_folder_move_failure_is_partial_success(tmp_path): ) def test_live_upload_small_mp3(): """Upload a small MP3 and confirm a transcript is produced.""" - import plaud_tools.session as session_mod + import plaud_tools.core.session as session_mod session_path = os.getenv("PLAUD_SESSION_PATH") audio_path = os.getenv("PLAUD_TEST_AUDIO_PATH") if not session_path or not audio_path: pytest.skip("Set PLAUD_SESSION_PATH and PLAUD_TEST_AUDIO_PATH to run this test.") - from plaud_tools.transcode import get_file_type + from plaud_tools.core.transcode import get_file_type store = session_mod.FileSessionStore(session_path) client = PlaudClient(SessionManager(store))