From fe415b060290110b313b6cb3ded6da923f448316 Mon Sep 17 00:00:00 2001 From: Juan Sugg Date: Sat, 28 Mar 2026 19:06:48 -0300 Subject: [PATCH 1/2] feat: add explicit dev asset root mode --- Makefile | 7 +- README.md | 13 +++ bin/clawops-dev | 14 ++++ docs/testing/operations.md | 9 ++ scripts/dev-env.sh | 31 +++++++ src/clawops/runtime_assets.py | 41 +++++---- .../repo/test_scripts_migration_surfaces.py | 15 +++- .../clawops/assets/test_dev_entrypoints.py | 83 +++++++++++++++++++ .../clawops/assets/test_runtime_assets.py | 43 +++++++++- .../unit/clawops/cli/test_root_defaults.py | 57 ++++++++++++- 10 files changed, 292 insertions(+), 21 deletions(-) create mode 100755 bin/clawops-dev create mode 100755 scripts/dev-env.sh create mode 100644 tests/suites/unit/clawops/assets/test_dev_entrypoints.py diff --git a/Makefile b/Makefile index 271581c8..e01f14d5 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ DOCTOR_ARGS ?= PREFERRED_PYTHON := $(shell $(PYTHON) src/clawops/platform_compat.py --field preferred_project_python_version 2>/dev/null) UV_SYNC := $(UV) sync $(if $(PREFERRED_PYTHON),--python $(PREFERRED_PYTHON),) -.PHONY: help install setup doctor dev fmt lint imports typecheck actionlint shellcheck precommit dev-check test test-unit test-integration test-contracts test-framework test-e2e test-hypermemory test-qdrant test-all test-governance compile start-sidecars stop-sidecars render-config verify context-index run-harness backup +.PHONY: help install setup doctor dev dev-shell fmt lint imports typecheck actionlint shellcheck precommit dev-check test test-unit test-integration test-contracts test-framework test-e2e test-hypermemory test-qdrant test-all test-governance compile start-sidecars stop-sidecars render-config verify context-index run-harness backup help: ## Show available targets. @awk 'BEGIN {FS = ":.*## "}; /^[a-zA-Z0-9_.-]+:.*## / {printf "%-16s %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -34,6 +34,9 @@ dev: ## Sync the locked dev environment and install pre-commit hooks. $(UV_SYNC) $(DEV_SYNC_FLAGS) $(PRE_COMMIT) install --install-hooks +dev-shell: install ## Open an interactive dev shell with repo-backed StrongClaw assets enabled. + @bash -lc 'source scripts/dev-env.sh && exec "$${SHELL:-/bin/bash}" -i' + fmt: ## Apply import sorting, lint autofixes, and formatting. $(RUN) isort src tests $(RUN) ruff check --fix src tests @@ -103,7 +106,7 @@ compile: ## Compile source and tests in the managed dev environment. $(RUN) python -m compileall -q src tests render-config: ## Render the OpenClaw config bundle. - $(RUN) clawops render-openclaw-config --repo-root . + $(RUN) clawops render-openclaw-config --asset-root . start-sidecars: ## Launch the sidecar services. $(RUN) clawops ops sidecars up diff --git a/README.md b/README.md index 250b259b..aeee5fcd 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,19 @@ Boundary override flags are now literal: - use `--source-root` for source-tree-only verification such as `clawops baseline` - use `--repo-root` only for repo-contract tooling such as `clawops repo`, `clawops worktree`, and `clawops supply-chain` +Package-safe runtime commands now default to the packaged asset bundle even +when you run them from inside a StrongClaw source checkout. To opt into +repo-backed assets for local development, export `STRONGCLAW_ASSET_ROOT` or use +the repo-local developer flow: + +```bash +source scripts/dev-env.sh +clawops-dev render-openclaw-config +``` + +`make dev-shell` opens an interactive shell with the same repo-backed asset +override and managed virtualenv activated. + By default, StrongClaw now renders and provisions the `hypermemory` stack. Set one embedding model name before you run the no-arg setup path: diff --git a/bin/clawops-dev b/bin/clawops-dev new file mode 100755 index 00000000..e7a289ab --- /dev/null +++ b/bin/clawops-dev @@ -0,0 +1,14 @@ +#!/usr/bin/env sh +set -eu + +script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +repo_root=$(CDPATH='' cd -- "$script_dir/.." && pwd) + +if [ ! -f "$repo_root/pyproject.toml" ] || [ ! -d "$repo_root/src/clawops" ]; then + printf '%s\n' "clawops-dev must live inside a StrongClaw source checkout." >&2 + exit 1 +fi + +export STRONGCLAW_ASSET_ROOT="${STRONGCLAW_ASSET_ROOT:-$repo_root}" + +exec uv run --project "$repo_root" clawops "$@" diff --git a/docs/testing/operations.md b/docs/testing/operations.md index 4104e117..e9cfdd8e 100644 --- a/docs/testing/operations.md +++ b/docs/testing/operations.md @@ -31,6 +31,15 @@ - Safe timeout wrapper: `uv run python -m tests.utils.scripts.pytest_safe --timeout 600 -q -m integration` +## Contributor Dev Shell + +- Source the repo-local developer environment: + `source scripts/dev-env.sh` +- Or open a prepared shell in one step: + `make dev-shell` +- After either flow, use `clawops-dev ...` to run the repo checkout against + repo-backed assets without changing the default installed/runtime behavior. + ## Common Triage - If the monkeypatch governance contract fails, migrate the test to diff --git a/scripts/dev-env.sh b/scripts/dev-env.sh new file mode 100755 index 00000000..7c42a711 --- /dev/null +++ b/scripts/dev-env.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env sh + +# shellcheck disable=SC2317 +_strongclaw_dev_env_return_or_exit() { + code="$1" + return "$code" 2>/dev/null || exit "$code" +} + +if [ ! -f "pyproject.toml" ] || [ ! -d "src/clawops" ]; then + printf '%s\n' "source scripts/dev-env.sh from the StrongClaw repository root." >&2 + _strongclaw_dev_env_return_or_exit 1 +fi + +repo_root=$(pwd -P) +venv_activate="$repo_root/.venv/bin/activate" + +if [ ! -f "$venv_activate" ]; then + printf '%s\n' "Managed environment not found at $venv_activate. Run \`uv sync --locked\` first." >&2 + _strongclaw_dev_env_return_or_exit 1 +fi + +case ":$PATH:" in + *":$repo_root/bin:"*) ;; + *) PATH="$repo_root/bin:$PATH" ;; +esac + +export PATH +export STRONGCLAW_ASSET_ROOT="$repo_root" + +# shellcheck source=/dev/null +. "$venv_activate" diff --git a/src/clawops/runtime_assets.py b/src/clawops/runtime_assets.py index c585dfc2..589a8079 100644 --- a/src/clawops/runtime_assets.py +++ b/src/clawops/runtime_assets.py @@ -3,6 +3,7 @@ from __future__ import annotations import dataclasses +import os import pathlib import shutil from typing import Final @@ -16,12 +17,10 @@ strongclaw_workspace_dir, strongclaw_worktrees_dir, ) -from clawops.root_detection import ( - STRONGCLAW_REPO_MARKERS, - discover_strongclaw_repo_root, -) +from clawops.root_detection import STRONGCLAW_REPO_MARKERS PACKAGED_ASSET_ROOT: Final[pathlib.Path] = pathlib.Path(__file__).resolve().parent / "assets" +ASSET_ROOT_ENV_VAR: Final[str] = "STRONGCLAW_ASSET_ROOT" PLATFORM_DIR_NAME: Final[str] = "platform" MEMORY_CONFIG_RELATIVE_DIR: Final[pathlib.Path] = pathlib.Path("platform/configs/memory") VARLOCK_CONFIG_RELATIVE_DIR: Final[pathlib.Path] = pathlib.Path("platform/configs/varlock") @@ -48,7 +47,7 @@ class RuntimeLayout: @property def uses_packaged_assets(self) -> bool: """Return whether runtime assets come from the installed package bundle.""" - return self.source_checkout_root is None + return self.asset_root == PACKAGED_ASSET_ROOT def _resolve_path(value: pathlib.Path | str) -> pathlib.Path: @@ -70,26 +69,36 @@ def _matches_source_checkout(root: pathlib.Path) -> bool: return all((root / marker).exists() for marker in STRONGCLAW_REPO_MARKERS) +def _configured_asset_root_override( + repo_root: pathlib.Path | str | None = None, +) -> pathlib.Path | None: + """Return the explicit asset-root override when one was supplied.""" + if repo_root is not None: + return _require_platform_root(_resolve_path(repo_root)) + configured = os.environ.get(ASSET_ROOT_ENV_VAR, "").strip() + if not configured: + return None + return _require_platform_root(_resolve_path(configured)) + + def resolve_asset_root(repo_root: pathlib.Path | str | None = None) -> pathlib.Path: """Return the effective runtime asset root.""" - if repo_root is not None: - return _resolve_path(repo_root) - source_root = discover_strongclaw_repo_root() - if source_root is not None: - return _require_platform_root(source_root) + override = _configured_asset_root_override(repo_root) + if override is not None: + return override return _require_platform_root(PACKAGED_ASSET_ROOT) def resolve_source_checkout_root( repo_root: pathlib.Path | str | None = None, ) -> pathlib.Path | None: - """Return the active StrongClaw source checkout when one exists.""" - if repo_root is not None: - candidate = _resolve_path(repo_root) - if _matches_source_checkout(candidate): - return candidate + """Return the active explicit StrongClaw source checkout when one exists.""" + override = _configured_asset_root_override(repo_root) + if override is None: return None - return discover_strongclaw_repo_root() + if _matches_source_checkout(override): + return override + return None def resolve_runtime_layout( diff --git a/tests/suites/contracts/repo/test_scripts_migration_surfaces.py b/tests/suites/contracts/repo/test_scripts_migration_surfaces.py index 5ed8fc50..acd0fb37 100644 --- a/tests/suites/contracts/repo/test_scripts_migration_surfaces.py +++ b/tests/suites/contracts/repo/test_scripts_migration_surfaces.py @@ -9,13 +9,26 @@ def test_makefile_uses_python_native_operational_targets() -> None: makefile = (REPO_ROOT / "Makefile").read_text(encoding="utf-8") assert "preferred_python.sh" not in makefile - assert "./scripts/" not in makefile + assert "scripts/ops/" not in makefile assert "PYTHONPATH=src" not in makefile assert "--extra dev" not in makefile assert "$(UV) run --locked pytest" in makefile + assert "dev-shell: install" in makefile assert "$(RUN) clawops ops sidecars up" in makefile assert "$(RUN) clawops baseline verify" in makefile assert "$(RUN) clawops recovery backup-create" in makefile + assert "$(RUN) clawops render-openclaw-config --asset-root ." in makefile + + +def test_repo_dev_entrypoints_enable_explicit_repo_asset_mode() -> None: + dev_env = (REPO_ROOT / "scripts" / "dev-env.sh").read_text(encoding="utf-8") + wrapper = (REPO_ROOT / "bin" / "clawops-dev").read_text(encoding="utf-8") + + assert 'export STRONGCLAW_ASSET_ROOT="$repo_root"' in dev_env + assert 'PATH="$repo_root/bin:$PATH"' in dev_env + assert '. "$venv_activate"' in dev_env + assert 'export STRONGCLAW_ASSET_ROOT="${STRONGCLAW_ASSET_ROOT:-$repo_root}"' in wrapper + assert 'exec uv run --project "$repo_root" clawops "$@"' in wrapper def test_service_templates_call_repo_venv_python() -> None: diff --git a/tests/suites/unit/clawops/assets/test_dev_entrypoints.py b/tests/suites/unit/clawops/assets/test_dev_entrypoints.py new file mode 100644 index 00000000..9e9d3947 --- /dev/null +++ b/tests/suites/unit/clawops/assets/test_dev_entrypoints.py @@ -0,0 +1,83 @@ +"""Unit tests for repo-local developer entrypoints.""" + +from __future__ import annotations + +import json +import os +import pathlib +import subprocess + +from tests.utils.helpers.repo import REPO_ROOT + + +def _write_fake_uv(fake_bin: pathlib.Path) -> None: + """Write one fake `uv` executable that records argv and asset-root env.""" + fake_bin.mkdir() + fake_uv = fake_bin / "uv" + fake_uv.write_text( + "\n".join( + ( + "#!/usr/bin/env python3", + "from __future__ import annotations", + "import json", + "import os", + "import pathlib", + "import sys", + "pathlib.Path(os.environ['TEST_CAPTURE_PATH']).write_text(", + " json.dumps({'argv': sys.argv[1:], 'asset_root': os.environ.get('STRONGCLAW_ASSET_ROOT')}),", + " encoding='utf-8',", + ")", + ) + ) + + "\n", + encoding="utf-8", + ) + fake_uv.chmod(0o755) + + +def test_clawops_dev_wrapper_exports_repo_asset_root_and_forwards_args( + tmp_path: pathlib.Path, +) -> None: + fake_bin = tmp_path / "fake-bin" + capture_path = tmp_path / "capture.json" + _write_fake_uv(fake_bin) + wrapper_path = REPO_ROOT / "bin" / "clawops-dev" + env = os.environ.copy() + env["PATH"] = f"{fake_bin}{os.pathsep}{env.get('PATH', '')}" + env["TEST_CAPTURE_PATH"] = str(capture_path) + + subprocess.run( + [str(wrapper_path), "doctor", "--json"], + check=True, + cwd=tmp_path, + env=env, + ) + + payload = json.loads(capture_path.read_text(encoding="utf-8")) + assert payload == { + "argv": ["run", "--project", str(REPO_ROOT), "clawops", "doctor", "--json"], + "asset_root": str(REPO_ROOT), + } + + +def test_clawops_dev_wrapper_respects_preconfigured_asset_root(tmp_path: pathlib.Path) -> None: + fake_bin = tmp_path / "fake-bin" + capture_path = tmp_path / "capture.json" + _write_fake_uv(fake_bin) + wrapper_path = REPO_ROOT / "bin" / "clawops-dev" + configured_asset_root = tmp_path / "configured-assets" + env = os.environ.copy() + env["PATH"] = f"{fake_bin}{os.pathsep}{env.get('PATH', '')}" + env["STRONGCLAW_ASSET_ROOT"] = str(configured_asset_root) + env["TEST_CAPTURE_PATH"] = str(capture_path) + + subprocess.run( + [str(wrapper_path), "config"], + check=True, + cwd=tmp_path, + env=env, + ) + + payload = json.loads(capture_path.read_text(encoding="utf-8")) + assert payload["argv"] == ["run", "--project", str(REPO_ROOT), "clawops", "config"] + assert payload["asset_root"] == str(configured_asset_root) diff --git a/tests/suites/unit/clawops/assets/test_runtime_assets.py b/tests/suites/unit/clawops/assets/test_runtime_assets.py index 84dd1682..536634f9 100644 --- a/tests/suites/unit/clawops/assets/test_runtime_assets.py +++ b/tests/suites/unit/clawops/assets/test_runtime_assets.py @@ -4,7 +4,14 @@ import pathlib -from clawops.runtime_assets import PACKAGED_ASSET_ROOT, resolve_asset_path, resolve_runtime_layout +import pytest + +from clawops.runtime_assets import ( + ASSET_ROOT_ENV_VAR, + PACKAGED_ASSET_ROOT, + resolve_asset_path, + resolve_runtime_layout, +) from tests.plugins.infrastructure.context import TestContext from tests.utils.helpers.repo import REPO_ROOT @@ -31,6 +38,40 @@ def test_runtime_layout_uses_source_checkout_when_explicit() -> None: assert layout.uses_packaged_assets is False +def test_runtime_layout_uses_packaged_assets_inside_source_checkout_by_default( + test_context: TestContext, +) -> None: + test_context.chdir(REPO_ROOT / "platform") + + layout = resolve_runtime_layout(home_dir=pathlib.Path.home()) + + assert layout.asset_root == PACKAGED_ASSET_ROOT + assert layout.source_checkout_root is None + assert layout.uses_packaged_assets is True + + +def test_runtime_layout_uses_source_checkout_when_env_override_is_set( + test_context: TestContext, +) -> None: + test_context.env.set(ASSET_ROOT_ENV_VAR, str(REPO_ROOT)) + + layout = resolve_runtime_layout(home_dir=pathlib.Path.home()) + + assert layout.asset_root == REPO_ROOT + assert layout.source_checkout_root == REPO_ROOT + assert layout.uses_packaged_assets is False + + +def test_runtime_layout_rejects_invalid_env_asset_root_override( + tmp_path: pathlib.Path, + test_context: TestContext, +) -> None: + test_context.env.set(ASSET_ROOT_ENV_VAR, str(tmp_path / "invalid-assets")) + + with pytest.raises(FileNotFoundError, match="StrongClaw asset root must contain platform/:"): + resolve_runtime_layout(home_dir=tmp_path / "home") + + def test_resolve_asset_path_accepts_explicit_asset_root(tmp_path: pathlib.Path) -> None: asset_root = tmp_path / "assets" target = asset_root / "platform" / "docs" / "guide.md" diff --git a/tests/suites/unit/clawops/cli/test_root_defaults.py b/tests/suites/unit/clawops/cli/test_root_defaults.py index 49c2ae2e..af58eac7 100644 --- a/tests/suites/unit/clawops/cli/test_root_defaults.py +++ b/tests/suites/unit/clawops/cli/test_root_defaults.py @@ -13,6 +13,7 @@ import clawops.openclaw_config as openclaw_config import clawops.strongclaw_baseline as strongclaw_baseline from clawops.root_detection import resolve_project_root, resolve_strongclaw_repo_root +from clawops.runtime_assets import ASSET_ROOT_ENV_VAR, PACKAGED_ASSET_ROOT from tests.plugins.infrastructure.context import TestContext @@ -50,7 +51,7 @@ def test_resolve_project_root_prefers_the_nearest_git_ancestor(tmp_path: pathlib assert resolved == project_root.resolve() -def test_render_openclaw_config_main_infers_repo_root_from_cwd( +def test_render_openclaw_config_main_uses_packaged_assets_from_source_checkout_by_default( tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str], test_context: TestContext, @@ -97,6 +98,60 @@ def _materialize_runtime_memory_configs( exit_code = openclaw_config.main(["--profile", "hypermemory", "--output", str(output_path)]) + assert exit_code == 0 + assert captured_repo_root == PACKAGED_ASSET_ROOT + assert json.loads(output_path.read_text(encoding="utf-8")) == {"ok": True} + assert "Rendered" in capsys.readouterr().out + + +def test_render_openclaw_config_main_honors_env_asset_root_override( + tmp_path: pathlib.Path, + capsys: pytest.CaptureFixture[str], + test_context: TestContext, +) -> None: + repo_root = _init_strongclaw_repo(tmp_path / "repo") + nested = repo_root / "platform" / "docs" + nested.mkdir(parents=True, exist_ok=True) + output_path = tmp_path / "openclaw.json" + captured_repo_root: pathlib.Path | None = None + + def _render_openclaw_profile( + *, + profile_name: str, + repo_root: pathlib.Path, + home_dir: pathlib.Path | None, + user_timezone: str | None = None, + extra_overlays: tuple[pathlib.Path, ...] = (), + ) -> dict[str, object]: + del profile_name, home_dir, user_timezone, extra_overlays + nonlocal captured_repo_root + captured_repo_root = repo_root + return {"ok": True} + + def _materialize_runtime_memory_configs( + *, + repo_root: pathlib.Path, + home_dir: pathlib.Path, + user_timezone: str | None = None, + ) -> tuple[pathlib.Path, pathlib.Path]: + del home_dir, user_timezone + return repo_root / "managed-memory.yaml", repo_root / "managed-memory.sqlite.yaml" + + test_context.chdir(nested) + test_context.env.set(ASSET_ROOT_ENV_VAR, str(repo_root)) + test_context.patch.patch_object( + openclaw_config, + "render_openclaw_profile", + new=_render_openclaw_profile, + ) + test_context.patch.patch_object( + openclaw_config, + "materialize_runtime_memory_configs", + new=_materialize_runtime_memory_configs, + ) + + exit_code = openclaw_config.main(["--profile", "hypermemory", "--output", str(output_path)]) + assert exit_code == 0 assert captured_repo_root == repo_root.resolve() assert json.loads(output_path.read_text(encoding="utf-8")) == {"ok": True} From b517fc68f994ce8d7449299c3fe4bd8f27ca2598 Mon Sep 17 00:00:00 2001 From: Juan Sugg Date: Sat, 28 Mar 2026 19:19:33 -0300 Subject: [PATCH 2/2] fix: tighten explicit asset root validation --- src/clawops/cli_roots.py | 4 +++- src/clawops/runtime_assets.py | 7 ++++++- src/clawops/workflow_runner.py | 9 ++++++--- .../unit/clawops/cli/test_root_flag_aliases.py | 6 ++++-- .../clawops/cli/test_setup_runtime_boundaries.py | 7 +++++-- tests/suites/unit/clawops/test_config_cli.py | 7 +++++-- tests/suites/unit/clawops/test_setup_cli.py | 15 +++++++++++---- tests/utils/helpers/assets.py | 11 +++++++++++ 8 files changed, 51 insertions(+), 15 deletions(-) create mode 100644 tests/utils/helpers/assets.py diff --git a/src/clawops/cli_roots.py b/src/clawops/cli_roots.py index 8ddd60d3..d4dce862 100644 --- a/src/clawops/cli_roots.py +++ b/src/clawops/cli_roots.py @@ -8,7 +8,7 @@ from typing import Final from clawops.root_detection import resolve_project_root, resolve_strongclaw_repo_root -from clawops.runtime_assets import resolve_asset_root +from clawops.runtime_assets import require_asset_root, resolve_asset_root DEPRECATED_REPO_ROOT_FLAG: Final[str] = "--repo-root" @@ -101,6 +101,8 @@ def resolve_asset_root_argument( command_name=command_name, canonical_flag="--asset-root", ) + if candidate is not None: + return require_asset_root(candidate) return resolve_asset_root(candidate) diff --git a/src/clawops/runtime_assets.py b/src/clawops/runtime_assets.py index 589a8079..4bd5ddb0 100644 --- a/src/clawops/runtime_assets.py +++ b/src/clawops/runtime_assets.py @@ -74,13 +74,18 @@ def _configured_asset_root_override( ) -> pathlib.Path | None: """Return the explicit asset-root override when one was supplied.""" if repo_root is not None: - return _require_platform_root(_resolve_path(repo_root)) + return _resolve_path(repo_root) configured = os.environ.get(ASSET_ROOT_ENV_VAR, "").strip() if not configured: return None return _require_platform_root(_resolve_path(configured)) +def require_asset_root(root: pathlib.Path | str) -> pathlib.Path: + """Resolve and validate one explicit runtime asset-root override.""" + return _require_platform_root(_resolve_path(root)) + + def resolve_asset_root(repo_root: pathlib.Path | str | None = None) -> pathlib.Path: """Return the effective runtime asset root.""" override = _configured_asset_root_override(repo_root) diff --git a/src/clawops/workflow_runner.py b/src/clawops/workflow_runner.py index 43b4b309..ad22fa78 100644 --- a/src/clawops/workflow_runner.py +++ b/src/clawops/workflow_runner.py @@ -39,6 +39,7 @@ ) from clawops.policy_engine import PolicyEngine from clawops.process_runner import run_command +from clawops.root_detection import DEFAULT_SOURCE_REPO_ROOT from clawops.runtime_assets import resolve_asset_path from clawops.typed_values import ( as_bool, @@ -105,9 +106,11 @@ def _default_context_pack_output(*, base_dir: pathlib.Path, step_name: str) -> p return scoped_state_dir(base_dir, category="context-packs") / f"{_safe_step_slug(step_name)}.md" -TRUSTED_WORKFLOW_ROOTS: tuple[pathlib.Path, ...] = ( - resolve_asset_path("platform/configs/workflows"), -) +_trusted_workflow_roots: list[pathlib.Path] = [resolve_asset_path("platform/configs/workflows")] +_source_workflow_root = (DEFAULT_SOURCE_REPO_ROOT / "platform" / "configs" / "workflows").resolve() +if _source_workflow_root.is_dir(): + _trusted_workflow_roots.append(_source_workflow_root) +TRUSTED_WORKFLOW_ROOTS: tuple[pathlib.Path, ...] = tuple(dict.fromkeys(_trusted_workflow_roots)) ALLOWED_WORKFLOW_KINDS = frozenset( { diff --git a/tests/suites/unit/clawops/cli/test_root_flag_aliases.py b/tests/suites/unit/clawops/cli/test_root_flag_aliases.py index d5c8bf84..c86b0430 100644 --- a/tests/suites/unit/clawops/cli/test_root_flag_aliases.py +++ b/tests/suites/unit/clawops/cli/test_root_flag_aliases.py @@ -33,13 +33,15 @@ def test_asset_root_legacy_repo_root_alias_warns( tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str], ) -> None: + asset_root = tmp_path / "assets" + (asset_root / "platform").mkdir(parents=True) parser = argparse.ArgumentParser() add_asset_root_argument(parser) - args = parser.parse_args(["--repo-root", str(tmp_path)]) + args = parser.parse_args(["--repo-root", str(asset_root)]) resolved = resolve_asset_root_argument(args, command_name="clawops config") - assert resolved == tmp_path.resolve() + assert resolved == asset_root.resolve() assert ( capsys.readouterr().err.strip() == "warning: --repo-root is deprecated for clawops config; use --asset-root." diff --git a/tests/suites/unit/clawops/cli/test_setup_runtime_boundaries.py b/tests/suites/unit/clawops/cli/test_setup_runtime_boundaries.py index 96b12a6f..8dad9e09 100644 --- a/tests/suites/unit/clawops/cli/test_setup_runtime_boundaries.py +++ b/tests/suites/unit/clawops/cli/test_setup_runtime_boundaries.py @@ -9,6 +9,7 @@ from clawops import cli as root_cli from clawops import setup_cli from tests.plugins.infrastructure.context import TestContext +from tests.utils.helpers.assets import make_asset_root def test_root_cli_setup_render_only_path_skips_model_auth( @@ -16,6 +17,7 @@ def test_root_cli_setup_render_only_path_skips_model_auth( test_context: TestContext, ) -> None: """The public CLI should not require model auth on a render-only setup path.""" + asset_root = make_asset_root(tmp_path / "assets") calls: list[str] = [] def _bootstrap_state_ready() -> bool: @@ -87,7 +89,7 @@ def _render_service_files(repo_root: pathlib.Path) -> dict[str, object]: test_context.patch.patch_object(setup_cli, "ensure_model_auth", new=_ensure_model_auth) test_context.patch.patch_object(setup_cli, "render_service_files", new=_render_service_files) - exit_code = root_cli.main(["setup", "--asset-root", str(tmp_path), "--no-activate-services"]) + exit_code = root_cli.main(["setup", "--asset-root", str(asset_root), "--no-activate-services"]) assert exit_code == 0 assert calls == [ @@ -105,6 +107,7 @@ def test_root_cli_doctor_bounded_path_skips_openclaw_runtime_audits( test_context: TestContext, ) -> None: """The public CLI should keep bounded doctor local when both skip flags are set.""" + asset_root = make_asset_root(tmp_path / "assets") class _OkReport: ok = True @@ -169,7 +172,7 @@ def _verify_channels(**kwargs: object) -> _OkReport: test_context.patch.patch_object(setup_cli, "verify_channels", new=_verify_channels) exit_code = root_cli.main( - ["doctor", "--asset-root", str(tmp_path), "--skip-runtime", "--no-model-probe"] + ["doctor", "--asset-root", str(asset_root), "--skip-runtime", "--no-model-probe"] ) output = capsys.readouterr().out diff --git a/tests/suites/unit/clawops/test_config_cli.py b/tests/suites/unit/clawops/test_config_cli.py index faa47faf..7f138c96 100644 --- a/tests/suites/unit/clawops/test_config_cli.py +++ b/tests/suites/unit/clawops/test_config_cli.py @@ -8,6 +8,7 @@ import pytest from clawops import config_cli +from tests.utils.helpers.assets import make_asset_root def test_memory_config_list_profiles_json(capsys: pytest.CaptureFixture[str]) -> None: @@ -28,6 +29,7 @@ def test_memory_config_set_profile_installs_assets_and_renders( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: + asset_root = make_asset_root(tmp_path / "assets") output_path = tmp_path / "openclaw.json" installed_calls: list[str] = [] @@ -79,7 +81,7 @@ def _test_materialize_runtime_memory_configs( exit_code = config_cli.main( [ "--asset-root", - str(tmp_path), + str(asset_root), "memory", "--set-profile", "openclaw-qmd", @@ -101,6 +103,7 @@ def test_memory_config_set_profile_skip_assets_only_renders( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: + asset_root = make_asset_root(tmp_path / "assets") output_path = tmp_path / "openclaw.json" def _render_openclaw_profile( @@ -136,7 +139,7 @@ def _test_materialize_runtime_memory_configs( exit_code = config_cli.main( [ "--asset-root", - str(tmp_path), + str(asset_root), "memory", "--set-profile", "hypermemory", diff --git a/tests/suites/unit/clawops/test_setup_cli.py b/tests/suites/unit/clawops/test_setup_cli.py index a7841cad..5ec0753f 100644 --- a/tests/suites/unit/clawops/test_setup_cli.py +++ b/tests/suites/unit/clawops/test_setup_cli.py @@ -7,12 +7,14 @@ import pytest from clawops import setup_cli +from tests.utils.helpers.assets import make_asset_root def test_setup_cli_auto_skips_bootstrap_when_state_exists( monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, ) -> None: + asset_root = make_asset_root(tmp_path / "assets") calls: list[str] = [] def _bootstrap_state_ready() -> bool: @@ -104,7 +106,7 @@ def _render_service_files(repo_root: pathlib.Path) -> dict[str, object]: _render_service_files, ) - exit_code = setup_cli.setup_main(["--asset-root", str(tmp_path), "--no-activate-services"]) + exit_code = setup_cli.setup_main(["--asset-root", str(asset_root), "--no-activate-services"]) assert exit_code == 0 assert calls == [ @@ -120,6 +122,7 @@ def test_setup_cli_keeps_model_auth_when_services_are_activated( monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, ) -> None: + asset_root = make_asset_root(tmp_path / "assets") calls: list[str] = [] def _bootstrap_state_ready() -> bool: @@ -191,7 +194,7 @@ def _verify_baseline(repo_root: pathlib.Path, *, runs_dir: pathlib.Path) -> None monkeypatch.setattr(setup_cli, "activate_services", _activate_services) monkeypatch.setattr(setup_cli, "verify_baseline", _verify_baseline) - exit_code = setup_cli.setup_main(["--asset-root", str(tmp_path), "--non-interactive"]) + exit_code = setup_cli.setup_main(["--asset-root", str(asset_root), "--non-interactive"]) assert exit_code == 0 assert calls == [ @@ -210,6 +213,8 @@ def test_doctor_cli_reports_failures_without_raising( tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str], ) -> None: + asset_root = make_asset_root(tmp_path / "assets") + class _OkResult: ok = True @@ -295,7 +300,7 @@ def _verify_channels(**kwargs: object) -> _OkReport: _verify_channels, ) - exit_code = setup_cli.doctor_main(["--asset-root", str(tmp_path), "--skip-runtime"]) + exit_code = setup_cli.doctor_main(["--asset-root", str(asset_root), "--skip-runtime"]) payload = capsys.readouterr().out assert exit_code == 1 @@ -307,6 +312,8 @@ def test_doctor_cli_skips_openclaw_runtime_audits_for_bounded_local_scan( tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str], ) -> None: + asset_root = make_asset_root(tmp_path / "assets") + class _OkReport: ok = True @@ -368,7 +375,7 @@ def _verify_channels(**kwargs: object) -> _OkReport: monkeypatch.setattr(setup_cli, "verify_channels", _verify_channels) exit_code = setup_cli.doctor_main( - ["--asset-root", str(tmp_path), "--skip-runtime", "--no-model-probe"] + ["--asset-root", str(asset_root), "--skip-runtime", "--no-model-probe"] ) payload = capsys.readouterr().out diff --git a/tests/utils/helpers/assets.py b/tests/utils/helpers/assets.py new file mode 100644 index 00000000..f9146675 --- /dev/null +++ b/tests/utils/helpers/assets.py @@ -0,0 +1,11 @@ +"""Reusable helpers for StrongClaw runtime asset tests.""" + +from __future__ import annotations + +from pathlib import Path + + +def make_asset_root(root: Path) -> Path: + """Create and return one minimal valid StrongClaw asset root.""" + (root / "platform").mkdir(parents=True, exist_ok=True) + return root