diff --git a/aw_cli/__main__.py b/aw_cli/__main__.py index c302be62..acea0eae 100644 --- a/aw_cli/__main__.py +++ b/aw_cli/__main__.py @@ -5,6 +5,7 @@ from pathlib import Path from datetime import datetime +import os import subprocess import click @@ -13,23 +14,44 @@ from typing import Optional +def _resolve_profile(profile: Optional[str], testing: bool) -> str: + """Return the active profile name, resolving --testing as an alias for 'testing'.""" + if profile: + return profile + if testing: + return "testing" + return "" + + @click.group() -@click.option("--testing", is_flag=True) -def main(testing: bool = False): - pass +@click.option( + "--profile", + default=None, + help="Named instance profile (e.g. 'testing', 'research').", +) +@click.option("--testing", is_flag=True, help="Alias for --profile testing.") +def main(profile: Optional[str] = None, testing: bool = False): + active = _resolve_profile(profile, testing) + if active: + os.environ["AW_PROFILE"] = active + # If no explicit flag, preserve any inherited AW_PROFILE from the environment. @main.command() @click.pass_context def qt(ctx): - return subprocess.call( - ["aw-qt"] + (["--testing"] if ctx.parent.params["testing"] else []) - ) + profile = ctx.parent.params.get("profile") + testing = ctx.parent.params.get("testing") + active = _resolve_profile(profile, testing) + args = ["aw-qt"] + if active: + args += ["--profile", active] + return subprocess.call(args) @main.command() def directories(): - # Print all directories + # Print all directories (respects AW_PROFILE set by the group callback) from aw_core.dirs import get_data_dir, get_config_dir, get_cache_dir, get_log_dir print("Directory paths used") @@ -60,23 +82,23 @@ def logs( ): from aw_core.dirs import get_log_dir - testing = ctx.parent.params["testing"] + # AW_PROFILE was set by the group callback, so get_log_dir returns the profile-specific dir. logdir: Path = Path(get_log_dir(None)) - # find the oldest logfile in each of the subdirectories in the logging directory, and print the last lines in each one. - if module_name: - print_oldest_log(logdir / module_name, testing, since, level) + _print_newest_log(logdir / module_name, since, level) else: for subdir in sorted(logdir.iterdir()): if subdir.is_dir(): - print_oldest_log(subdir, testing, since, level) + _print_newest_log(subdir, since, level) -def print_oldest_log(path, testing, since, level): - path = find_oldest_log(path, testing) - if path: - print_log(path, since, level) +def _print_newest_log( + path: Path, since: Optional[datetime], level: Optional[str] +) -> None: + logfile = find_oldest_log(path) + if logfile: + print_log(logfile, since, level) else: print(f"No logfile found in {path}") diff --git a/aw_cli/log.py b/aw_cli/log.py index e55593ef..ee8deb9f 100644 --- a/aw_cli/log.py +++ b/aw_cli/log.py @@ -39,20 +39,19 @@ def print_log( print(f" (Filtered {lines_printed}/{len(lines)} lines)") -def find_oldest_log(path: Path, testing=False) -> Path: +def find_oldest_log(path: Path) -> Optional[Path]: + """Return the most-recently-modified log file under *path*. + + Since aw-core #149 each profile runs in its own platform directory + (``activitywatch-``), so no filename-based profile filtering is + needed here — every ``.log`` file in *path* belongs to the active profile. + """ if not path.is_dir(): - return + return None - logfiles = [ - f - for f in path.iterdir() - if f.is_file() - and f.name.endswith(".log") - and ("testing" in f.name if testing else "testing" not in f.name) - ] + logfiles = [f for f in path.iterdir() if f.is_file() and f.name.endswith(".log")] if not logfiles: - return + return None logfiles.sort(key=lambda f: f.stat().st_mtime) - logfile = logfiles[-1] - return logfile + return logfiles[-1] diff --git a/tests/test_cli_log.py b/tests/test_cli_log.py new file mode 100644 index 00000000..9ee5b762 --- /dev/null +++ b/tests/test_cli_log.py @@ -0,0 +1,64 @@ +"""Tests for aw_cli.log.find_oldest_log after profile isolation (#149). + +With per-profile appname dirs, every .log file in a module dir belongs to the +active profile — no filename-based filtering is needed. +""" + +import time +from pathlib import Path + +import pytest + +from aw_cli.log import find_oldest_log + +from . import context # noqa: F401 + + +@pytest.fixture() +def log_dir(tmp_path: Path) -> Path: + return tmp_path / "aw-server" + + +def test_returns_none_for_missing_dir(tmp_path: Path) -> None: + assert find_oldest_log(tmp_path / "nonexistent") is None + + +def test_returns_none_for_empty_dir(log_dir: Path) -> None: + log_dir.mkdir() + assert find_oldest_log(log_dir) is None + + +def test_returns_single_log(log_dir: Path) -> None: + log_dir.mkdir() + f = log_dir / "aw-server_2026-01-01.log" + f.write_text("line\n") + assert find_oldest_log(log_dir) == f + + +def test_returns_newest_by_mtime(log_dir: Path) -> None: + log_dir.mkdir() + old = log_dir / "aw-server_old.log" + new = log_dir / "aw-server_new.log" + old.write_text("old\n") + time.sleep(0.01) + new.write_text("new\n") + assert find_oldest_log(log_dir) == new + + +def test_ignores_non_log_files(log_dir: Path) -> None: + log_dir.mkdir() + (log_dir / "notes.txt").write_text("not a log") + assert find_oldest_log(log_dir) is None + + +def test_no_profile_filtering_in_filenames(log_dir: Path) -> None: + """All .log files in the dir belong to the active profile — no name filter.""" + log_dir.mkdir() + default_log = log_dir / "aw-server_2026-01-01.log" + default_log.write_text("line\n") + # A file named with 'testing' in its name is still returned; + # profile selection happens at the directory level, not the filename level. + testing_named = log_dir / "aw-server_testing_2026-01-02.log" + time.sleep(0.01) + testing_named.write_text("testing-named\n") + assert find_oldest_log(log_dir) == testing_named