From 768fdfbbffc45db4d9c6685ffb958fd377d4ea2b Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 15:36:08 +0000 Subject: [PATCH 1/2] feat: autodetect ArchML workspace by walking up the filesystem hierarchy Implement a `find_workspace_root` function in `workspace/config.py` that walks up the directory tree from a given start directory until it finds `.archml-workspace.yaml`, enabling users to run `archml` commands from any subdirectory of a workspace without specifying its location explicitly. Updated all four CLI commands (`check`, `serve`, `sync-remote`, `update-remote`) to call `find_workspace_root` when the workspace config file is not found at the given directory. The dead `.archml-workspace` marker-file check in `sync-remote` and `update-remote` (a file never created by `archml init`) is replaced by this unified detection mechanism. Added unit tests for `find_workspace_root` and integration tests verifying that each CLI command can autodetect a workspace located in a parent directory. Closes #44 Co-authored-by: Andi Hellmund --- src/archml/cli/main.py | 74 +++++++++++++++++-------------- src/archml/workspace/config.py | 25 +++++++++++ tests/cli/test_main.py | 81 +++++++++++++++++++++++----------- tests/workspace/test_config.py | 54 +++++++++++++++++++++++ 4 files changed, 176 insertions(+), 58 deletions(-) diff --git a/src/archml/cli/main.py b/src/archml/cli/main.py index 5530396..de1a0f6 100644 --- a/src/archml/cli/main.py +++ b/src/archml/cli/main.py @@ -167,7 +167,7 @@ def _cmd_init(args: argparse.Namespace) -> int: def _cmd_check(args: argparse.Namespace) -> int: """Handle the check subcommand.""" - from archml.workspace.config import GitPathImport, LocalPathImport + from archml.workspace.config import GitPathImport, LocalPathImport, find_workspace_root directory = Path(args.directory).resolve() @@ -178,11 +178,15 @@ def _cmd_check(args: argparse.Namespace) -> int: workspace_yaml = directory / ".archml-workspace.yaml" if not workspace_yaml.exists(): - print( - f"Error: no ArchML workspace found at '{directory}'. Run 'archml init' to initialize a workspace.", - file=sys.stderr, - ) - return 1 + root = find_workspace_root(directory) + if root is None: + print( + f"Error: no ArchML workspace found at '{directory}' or any parent directory. Run 'archml init' to initialize a workspace.", + file=sys.stderr, + ) + return 1 + directory = root + workspace_yaml = directory / ".archml-workspace.yaml" # The empty-string key represents the workspace root for non-mnemonic imports. source_import_map: dict[str, Path] = {"": directory} @@ -244,6 +248,8 @@ def _cmd_check(args: argparse.Namespace) -> int: def _cmd_serve(args: argparse.Namespace) -> int: """Handle the serve subcommand.""" + from archml.workspace.config import find_workspace_root + directory = Path(args.directory).resolve() if not directory.exists(): @@ -253,11 +259,15 @@ def _cmd_serve(args: argparse.Namespace) -> int: workspace_yaml = directory / ".archml-workspace.yaml" if not workspace_yaml.exists(): - print( - f"Error: no ArchML workspace found at '{directory}'. Run 'archml init' to initialize a workspace.", - file=sys.stderr, - ) - return 1 + root = find_workspace_root(directory) + if root is None: + print( + f"Error: no ArchML workspace found at '{directory}' or any parent directory. Run 'archml init' to initialize a workspace.", + file=sys.stderr, + ) + return 1 + directory = root + workspace_yaml = directory / ".archml-workspace.yaml" from archml.webui.app import create_app @@ -269,7 +279,7 @@ def _cmd_serve(args: argparse.Namespace) -> int: def _cmd_sync_remote(args: argparse.Namespace) -> int: """Handle the sync-remote subcommand.""" - from archml.workspace.config import GitPathImport + from archml.workspace.config import GitPathImport, find_workspace_root from archml.workspace.git_ops import GitError, clone_at_commit, get_current_commit from archml.workspace.lockfile import LOCKFILE_NAME, LockfileError, load_lockfile @@ -279,18 +289,17 @@ def _cmd_sync_remote(args: argparse.Namespace) -> int: print(f"Error: directory '{directory}' does not exist.", file=sys.stderr) return 1 - workspace_file = directory / ".archml-workspace" - if not workspace_file.exists(): - print( - f"Error: no ArchML workspace found at '{directory}'. Run 'archml init' to initialize a workspace.", - file=sys.stderr, - ) - return 1 - workspace_yaml = directory / ".archml-workspace.yaml" if not workspace_yaml.exists(): - print("No workspace configuration found. Nothing to sync.") - return 0 + root = find_workspace_root(directory) + if root is None: + print( + f"Error: no ArchML workspace found at '{directory}' or any parent directory. Run 'archml init' to initialize a workspace.", + file=sys.stderr, + ) + return 1 + directory = root + workspace_yaml = directory / ".archml-workspace.yaml" try: config = load_workspace_config(workspace_yaml) @@ -357,7 +366,7 @@ def _cmd_sync_remote(args: argparse.Namespace) -> int: def _cmd_update_remote(args: argparse.Namespace) -> int: """Handle the update-remote subcommand.""" - from archml.workspace.config import GitPathImport + from archml.workspace.config import GitPathImport, find_workspace_root from archml.workspace.git_ops import GitError, is_commit_hash, resolve_commit from archml.workspace.lockfile import ( LOCKFILE_NAME, @@ -374,18 +383,17 @@ def _cmd_update_remote(args: argparse.Namespace) -> int: print(f"Error: directory '{directory}' does not exist.", file=sys.stderr) return 1 - workspace_file = directory / ".archml-workspace" - if not workspace_file.exists(): - print( - f"Error: no ArchML workspace found at '{directory}'. Run 'archml init' to initialize a workspace.", - file=sys.stderr, - ) - return 1 - workspace_yaml = directory / ".archml-workspace.yaml" if not workspace_yaml.exists(): - print("No workspace configuration found. Nothing to update.") - return 0 + root = find_workspace_root(directory) + if root is None: + print( + f"Error: no ArchML workspace found at '{directory}' or any parent directory. Run 'archml init' to initialize a workspace.", + file=sys.stderr, + ) + return 1 + directory = root + workspace_yaml = directory / ".archml-workspace.yaml" try: config = load_workspace_config(workspace_yaml) diff --git a/src/archml/workspace/config.py b/src/archml/workspace/config.py index 2daf245..93ac746 100644 --- a/src/archml/workspace/config.py +++ b/src/archml/workspace/config.py @@ -49,6 +49,31 @@ class WorkspaceConfig(BaseModel): source_imports: list[LocalPathImport | GitPathImport] = Field(alias="source-imports", default_factory=list) +WORKSPACE_CONFIG_FILENAME = ".archml-workspace.yaml" + + +def find_workspace_root(start_dir: Path) -> Path | None: + """Walk up the directory tree to find the ArchML workspace root. + + Searches for a .archml-workspace.yaml file starting from start_dir and + ascending through parent directories until the filesystem root is reached. + + Args: + start_dir: Directory to start the search from. + + Returns: + The directory containing .archml-workspace.yaml, or None if not found. + """ + current = start_dir.resolve() + while True: + if (current / WORKSPACE_CONFIG_FILENAME).exists(): + return current + parent = current.parent + if parent == current: + return None + current = parent + + def load_workspace_config(path: Path) -> WorkspaceConfig: """Load and validate a workspace configuration file. diff --git a/tests/cli/test_main.py b/tests/cli/test_main.py index 96aaa54..671ae9a 100644 --- a/tests/cli/test_main.py +++ b/tests/cli/test_main.py @@ -201,13 +201,29 @@ def test_check_invalid_workspace_yaml( def test_check_fails_if_no_workspace(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - """check exits with error code 1 when no workspace file is found.""" + """check exits with error code 1 when no workspace file is found anywhere in the tree.""" monkeypatch.setattr(sys, "argv", ["archml", "check", str(tmp_path)]) with pytest.raises(SystemExit) as exc_info: main() assert exc_info.value.code == 1 +def test_check_autodetects_workspace_in_parent_directory( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """check finds the workspace by walking up from a subdirectory.""" + (tmp_path / ".archml-workspace.yaml").write_text("build-directory: .archml-build\n") + (tmp_path / "arch.archml").write_text("component MyComponent {}\n") + subdir = tmp_path / "src" / "components" + subdir.mkdir(parents=True) + monkeypatch.setattr(sys, "argv", ["archml", "check", str(subdir)]) + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "No issues found." in captured.out + + def test_check_fails_if_directory_does_not_exist(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """check exits with error code 1 when directory does not exist.""" missing = tmp_path / "nonexistent" @@ -298,13 +314,31 @@ def test_check_excludes_build_directory_from_scan( def test_serve_fails_if_no_workspace(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - """serve exits with error code 1 when no workspace file is found.""" + """serve exits with error code 1 when no workspace file is found anywhere in the tree.""" monkeypatch.setattr(sys, "argv", ["archml", "serve", str(tmp_path)]) with pytest.raises(SystemExit) as exc_info: main() assert exc_info.value.code == 1 +def test_serve_autodetects_workspace_in_parent_directory( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """serve finds the workspace by walking up from a subdirectory.""" + (tmp_path / ".archml-workspace.yaml").write_text("build-directory: .archml-build\n") + subdir = tmp_path / "src" / "components" + subdir.mkdir(parents=True) + monkeypatch.setattr(sys, "argv", ["archml", "serve", str(subdir)]) + mock_app = MagicMock() + with ( + patch("archml.webui.app.create_app", return_value=mock_app), + pytest.raises(SystemExit) as exc_info, + ): + main() + assert exc_info.value.code == 0 + mock_app.run.assert_called_once() + + def test_serve_fails_if_directory_does_not_exist(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """serve exits with error code 1 when directory does not exist.""" missing = tmp_path / "nonexistent" @@ -368,18 +402,23 @@ def test_sync_remote_fails_if_directory_does_not_exist(tmp_path: Path, monkeypat assert exc_info.value.code == 1 -def test_sync_remote_no_workspace_yaml(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - """sync-remote exits 0 with a message when no workspace YAML is present.""" - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") - monkeypatch.setattr(sys, "argv", ["archml", "sync-remote", str(tmp_path)]) +def test_sync_remote_autodetects_workspace_in_parent( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """sync-remote finds the workspace by walking up from a subdirectory.""" + (tmp_path / ".archml-workspace.yaml").write_text("build-directory: build\n") + subdir = tmp_path / "src" / "components" + subdir.mkdir(parents=True) + monkeypatch.setattr(sys, "argv", ["archml", "sync-remote", str(subdir)]) with pytest.raises(SystemExit) as exc_info: main() assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "Nothing to sync" in captured.out def test_sync_remote_no_git_imports(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """sync-remote exits 0 when no git imports are configured.""" - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text("build-directory: build\n") monkeypatch.setattr(sys, "argv", ["archml", "sync-remote", str(tmp_path)]) with pytest.raises(SystemExit) as exc_info: @@ -389,7 +428,6 @@ def test_sync_remote_no_git_imports(tmp_path: Path, monkeypatch: pytest.MonkeyPa def test_sync_remote_fails_without_lockfile(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """sync-remote exits 1 when the lockfile is missing.""" - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text( "build-directory: build\n" "source-imports:\n" @@ -407,7 +445,6 @@ def test_sync_remote_fails_if_repo_not_in_lockfile( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: """sync-remote exits 1 when a configured repo is missing from the lockfile.""" - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text( "build-directory: build\n" "source-imports:\n" @@ -428,7 +465,6 @@ def test_sync_remote_skips_repo_already_at_pinned_commit( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: """sync-remote skips a repo that is already at the pinned commit.""" - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text( "build-directory: build\n" "source-imports:\n" @@ -458,7 +494,6 @@ def test_sync_remote_clones_repo( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: """sync-remote calls clone_at_commit when repo is not at the pinned commit.""" - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text( "build-directory: build\n" "source-imports:\n" @@ -494,7 +529,6 @@ def test_sync_remote_reports_error_on_clone_failure( """sync-remote exits 1 and reports an error if clone_at_commit fails.""" from archml.workspace.git_ops import GitError - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text( "build-directory: build\n" "source-imports:\n" @@ -523,7 +557,6 @@ def test_sync_remote_reports_error_on_clone_failure( def test_sync_remote_uses_custom_sync_directory(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """sync-remote uses the remote-sync-directory from workspace config.""" - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text( "build-directory: build\n" "remote-sync-directory: custom-remotes\n" @@ -574,18 +607,23 @@ def test_update_remote_fails_if_directory_does_not_exist(tmp_path: Path, monkeyp assert exc_info.value.code == 1 -def test_update_remote_no_workspace_yaml(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - """update-remote exits 0 with a message when no workspace YAML is present.""" - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") - monkeypatch.setattr(sys, "argv", ["archml", "update-remote", str(tmp_path)]) +def test_update_remote_autodetects_workspace_in_parent( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """update-remote finds the workspace by walking up from a subdirectory.""" + (tmp_path / ".archml-workspace.yaml").write_text("build-directory: build\n") + subdir = tmp_path / "src" / "components" + subdir.mkdir(parents=True) + monkeypatch.setattr(sys, "argv", ["archml", "update-remote", str(subdir)]) with pytest.raises(SystemExit) as exc_info: main() assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "Nothing to update" in captured.out def test_update_remote_no_git_imports(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """update-remote exits 0 when no git imports are configured.""" - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text("build-directory: build\n") monkeypatch.setattr(sys, "argv", ["archml", "update-remote", str(tmp_path)]) with pytest.raises(SystemExit) as exc_info: @@ -596,7 +634,6 @@ def test_update_remote_no_git_imports(tmp_path: Path, monkeypatch: pytest.Monkey def test_update_remote_creates_lockfile_from_branch(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """update-remote creates a lockfile by resolving branch revisions.""" resolved_commit = "c" * 40 - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text( "build-directory: build\n" "source-imports:\n" @@ -625,7 +662,6 @@ def test_update_remote_creates_lockfile_from_branch(tmp_path: Path, monkeypatch: def test_update_remote_pins_commit_hash_without_network(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """update-remote does not call git for revisions that are already commit hashes.""" - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text( "build-directory: build\n" "source-imports:\n" @@ -652,7 +688,6 @@ def test_update_remote_updates_existing_lockfile(tmp_path: Path, monkeypatch: py """update-remote updates an existing lockfile with new commits.""" old_commit = "0" * 40 new_commit = "1" * 40 - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text( "build-directory: build\n" "source-imports:\n" @@ -687,7 +722,6 @@ def test_update_remote_exits_1_on_resolution_failure( """update-remote exits 1 if a revision cannot be resolved.""" from archml.workspace.git_ops import GitError - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text( "build-directory: build\n" "source-imports:\n" @@ -714,7 +748,6 @@ def test_check_command_uses_synced_remote_repos( remote_dir.mkdir(parents=True) (remote_dir / "api.archml").write_text("interface PaymentAPI { field amount: Decimal }\n") - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text( "build-directory: build\n" "source-imports:\n" @@ -745,7 +778,6 @@ def test_check_command_loads_remote_repo_mnemonics( "build-directory: build\nsource-imports:\n - name: lib\n local-path: src/lib\n" ) - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text( "build-directory: build\n" "source-imports:\n" @@ -775,7 +807,6 @@ def test_check_command_warns_on_invalid_remote_workspace_yaml( # Malformed remote workspace config (remote_dir / ".archml-workspace.yaml").write_text("bad yaml: [unterminated\n") - (tmp_path / ".archml-workspace").write_text("[workspace]\nversion = '1'\n") (tmp_path / ".archml-workspace.yaml").write_text( "build-directory: build\n" "source-imports:\n" diff --git a/tests/workspace/test_config.py b/tests/workspace/test_config.py index 02b11b9..d73b81a 100644 --- a/tests/workspace/test_config.py +++ b/tests/workspace/test_config.py @@ -10,6 +10,7 @@ LocalPathImport, WorkspaceConfig, WorkspaceConfigError, + find_workspace_root, load_workspace_config, ) @@ -228,3 +229,56 @@ def test_load_config_default_remote_sync_directory(tmp_path): config = load_workspace_config(cfg_file) assert config.remote_sync_directory == ".archml-remotes" + + +def test_find_workspace_root_in_current_directory(tmp_path): + """find_workspace_root returns the directory itself when it contains the workspace file.""" + (tmp_path / ".archml-workspace.yaml").write_text("build-directory: build\n", encoding="utf-8") + + result = find_workspace_root(tmp_path) + + assert result == tmp_path + + +def test_find_workspace_root_in_parent_directory(tmp_path): + """find_workspace_root walks up and finds the workspace in a parent directory.""" + (tmp_path / ".archml-workspace.yaml").write_text("build-directory: build\n", encoding="utf-8") + child_dir = tmp_path / "subdir" / "nested" + child_dir.mkdir(parents=True) + + result = find_workspace_root(child_dir) + + assert result == tmp_path + + +def test_find_workspace_root_returns_none_when_not_found(tmp_path): + """find_workspace_root returns None when no workspace file exists anywhere in the tree.""" + child_dir = tmp_path / "subdir" + child_dir.mkdir() + + result = find_workspace_root(child_dir) + + assert result is None + + +def test_find_workspace_root_uses_nearest_ancestor(tmp_path): + """find_workspace_root returns the closest (innermost) workspace directory.""" + (tmp_path / ".archml-workspace.yaml").write_text("build-directory: outer\n", encoding="utf-8") + inner_dir = tmp_path / "inner" + inner_dir.mkdir() + (inner_dir / ".archml-workspace.yaml").write_text("build-directory: inner\n", encoding="utf-8") + nested = inner_dir / "deep" + nested.mkdir() + + result = find_workspace_root(nested) + + assert result == inner_dir + + +def test_find_workspace_root_returns_given_dir_when_it_is_the_root(tmp_path): + """find_workspace_root resolves the start directory and finds the workspace in it.""" + (tmp_path / ".archml-workspace.yaml").write_text("build-directory: build\n", encoding="utf-8") + + result = find_workspace_root(tmp_path / ".") + + assert result == tmp_path From ef629f47b219c64334c5d3574300893796ce15ee Mon Sep 17 00:00:00 2001 From: Andi Hellmund Date: Sun, 1 Mar 2026 22:42:09 +0100 Subject: [PATCH 2/2] finalize --- src/archml/cli/main.py | 12 ++++++++---- tests/cli/test_main.py | 4 +--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/archml/cli/main.py b/src/archml/cli/main.py index de1a0f6..7af219d 100644 --- a/src/archml/cli/main.py +++ b/src/archml/cli/main.py @@ -181,7 +181,8 @@ def _cmd_check(args: argparse.Namespace) -> int: root = find_workspace_root(directory) if root is None: print( - f"Error: no ArchML workspace found at '{directory}' or any parent directory. Run 'archml init' to initialize a workspace.", + f"Error: no ArchML workspace found at '{directory}' or any parent directory." + "Run 'archml init' to initialize a workspace.", file=sys.stderr, ) return 1 @@ -262,7 +263,8 @@ def _cmd_serve(args: argparse.Namespace) -> int: root = find_workspace_root(directory) if root is None: print( - f"Error: no ArchML workspace found at '{directory}' or any parent directory. Run 'archml init' to initialize a workspace.", + f"Error: no ArchML workspace found at '{directory}' or any parent directory." + "Run 'archml init' to initialize a workspace.", file=sys.stderr, ) return 1 @@ -294,7 +296,8 @@ def _cmd_sync_remote(args: argparse.Namespace) -> int: root = find_workspace_root(directory) if root is None: print( - f"Error: no ArchML workspace found at '{directory}' or any parent directory. Run 'archml init' to initialize a workspace.", + f"Error: no ArchML workspace found at '{directory}' or any parent directory." + "Run 'archml init' to initialize a workspace.", file=sys.stderr, ) return 1 @@ -388,7 +391,8 @@ def _cmd_update_remote(args: argparse.Namespace) -> int: root = find_workspace_root(directory) if root is None: print( - f"Error: no ArchML workspace found at '{directory}' or any parent directory. Run 'archml init' to initialize a workspace.", + f"Error: no ArchML workspace found at '{directory}' or any parent directory." + "Run 'archml init' to initialize a workspace.", file=sys.stderr, ) return 1 diff --git a/tests/cli/test_main.py b/tests/cli/test_main.py index 671ae9a..8f802be 100644 --- a/tests/cli/test_main.py +++ b/tests/cli/test_main.py @@ -321,9 +321,7 @@ def test_serve_fails_if_no_workspace(tmp_path: Path, monkeypatch: pytest.MonkeyP assert exc_info.value.code == 1 -def test_serve_autodetects_workspace_in_parent_directory( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_serve_autodetects_workspace_in_parent_directory(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """serve finds the workspace by walking up from a subdirectory.""" (tmp_path / ".archml-workspace.yaml").write_text("build-directory: .archml-build\n") subdir = tmp_path / "src" / "components"