Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/memory-plugin-verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ jobs:
enable-cache: true
- run: uv sync --locked
- run: PYTHONPATH=src uv run python -m clawops config --asset-root . memory --set-profile memory-lancedb-pro --output "$RUNNER_TEMP/openclaw.json"
- run: python3 ./tests/scripts/memory_plugin_verification.py run-clawops-memory-migration --repo-root . --runner-temp "$RUNNER_TEMP"
- run: python3 ./tests/scripts/memory_plugin_verification.py run-vendored-host-checks --repo-root . --package-spec "openclaw@2026.3.13"
verify-hypermemory-qdrant:
name: Run Hypermemory Qdrant Checks
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
run: PYTHONPATH=src uv run python -m compileall -q src tests
- name: Create nightly artifact directory
run: mkdir -p "$RUNNER_TEMP/strongclaw/nightly"
- name: Render nightly OpenClaw profile matrix
run: ./tests/scripts/compatibility_matrix.py assert-openclaw-profiles --repo-root . --runner-temp "${RUNNER_TEMP}"
- name: Run nightly security harness
run: PYTHONPATH=src uv run python -m clawops harness --suite platform/configs/harness/security_regressions.yaml --output "$RUNNER_TEMP/strongclaw/nightly/security.jsonl"
- name: Run nightly policy harness
Expand Down
14 changes: 14 additions & 0 deletions tests/scripts/compatibility_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from tests.utils.helpers._ci_workflows.compatibility import ( # noqa: E402
assert_hypermemory_config,
assert_lossless_claw_installed,
assert_openclaw_profiles_render,
prepare_setup_smoke,
)

Expand Down Expand Up @@ -46,6 +47,13 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
)
config_parser.add_argument("--tmp-root", type=Path, required=True)

profile_parser = subparsers.add_parser(
"assert-openclaw-profiles",
help="Render all OpenClaw profiles for nightly validation.",
)
profile_parser.add_argument("--repo-root", type=Path, default=REPO_ROOT)
profile_parser.add_argument("--runner-temp", type=Path, required=True)

return parser.parse_args(argv)


Expand All @@ -70,6 +78,12 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "assert-hypermemory-config":
assert_hypermemory_config(Path(args.tmp_root).expanduser().resolve())
return 0
if args.command == "assert-openclaw-profiles":
assert_openclaw_profiles_render(
Path(args.repo_root).expanduser().resolve(),
Path(args.runner_temp).expanduser().resolve(),
)
return 0
except CiWorkflowError as exc:
print(f"compatibility-matrix error: {exc}", file=sys.stderr)
return 1
Expand Down
18 changes: 18 additions & 0 deletions tests/scripts/memory_plugin_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from tests.utils.helpers._ci_workflows.common import CiWorkflowError # noqa: E402
from tests.utils.helpers._ci_workflows.memory_plugin import ( # noqa: E402
DEFAULT_OPENCLAW_PACKAGE_SPEC,
run_clawops_memory_migration,
run_vendored_host_checks,
wait_for_qdrant,
)
Expand All @@ -33,6 +34,13 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
vendored_parser.add_argument("--repo-root", type=Path, default=REPO_ROOT)
vendored_parser.add_argument("--package-spec", default=DEFAULT_OPENCLAW_PACKAGE_SPEC)

migration_parser = subparsers.add_parser(
"run-clawops-memory-migration",
help="Run clawops memory migration in dry-run mode.",
)
migration_parser.add_argument("--repo-root", type=Path, default=REPO_ROOT)
migration_parser.add_argument("--runner-temp", type=Path)

qdrant_parser = subparsers.add_parser("wait-for-qdrant", help="Wait for Qdrant readiness.")
qdrant_parser.add_argument("--url", required=True)
qdrant_parser.add_argument("--attempts", type=int, default=30)
Expand All @@ -51,6 +59,16 @@ def main(argv: list[str] | None = None) -> int:
package_spec=str(args.package_spec),
)
return 0
if args.command == "run-clawops-memory-migration":
run_clawops_memory_migration(
Path(args.repo_root).expanduser().resolve(),
runner_temp=(
Path(args.runner_temp).expanduser().resolve()
if args.runner_temp is not None
else None
),
)
return 0
if args.command == "wait-for-qdrant":
wait_for_qdrant(
str(args.url),
Expand Down
6 changes: 6 additions & 0 deletions tests/suites/contracts/repo/test_ci_workflow_surfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,18 @@ def test_remaining_workflow_logic_routes_through_semantic_scripts() -> None:
"""Refactored workflow lanes should route operational logic through semantic scripts."""
compatibility = _workflow_text("compatibility-matrix.yml")
memory_plugin = _workflow_text("memory-plugin-verification.yml")
nightly = _workflow_text("nightly.yml")
security = _workflow_text("security.yml")
release = _workflow_text("release.yml")

assert "./tests/scripts/compatibility_matrix.py prepare-setup-smoke" in compatibility
assert "./tests/scripts/compatibility_matrix.py assert-lossless-claw" in compatibility
assert "./tests/scripts/compatibility_matrix.py assert-hypermemory-config" in compatibility
assert "./tests/scripts/compatibility_matrix.py assert-openclaw-profiles" in nightly
assert (
"./tests/scripts/memory_plugin_verification.py run-clawops-memory-migration"
in memory_plugin
)
assert "./tests/scripts/memory_plugin_verification.py run-vendored-host-checks" in memory_plugin
assert "./tests/scripts/memory_plugin_verification.py wait-for-qdrant" in memory_plugin
assert "./tests/scripts/security_workflow.py write-coverage-summary" in security
Expand Down
71 changes: 71 additions & 0 deletions tests/suites/unit/ci/test_compatibility_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,74 @@ def fake_prepare_setup_smoke(
github_env_file.resolve(),
)
]


def test_assert_openclaw_profiles_render_writes_one_file_per_profile(
test_context: TestContext,
tmp_path: Path,
) -> None:
"""Nightly profile validation should render every OpenClaw profile to disk."""
repo_root = tmp_path / "repo"
repo_root.mkdir()
runner_temp = tmp_path / "runner-temp"
seen_calls: list[tuple[str, Path, Path]] = []

def fake_render_openclaw_profile(
*,
profile_name: str,
repo_root: Path,
home_dir: Path,
) -> dict[str, str]:
seen_calls.append((profile_name, repo_root, home_dir))
return {"profile": profile_name}

test_context.patch.patch_object(
compatibility_helpers,
"render_openclaw_profile",
new=fake_render_openclaw_profile,
)

rendered_profiles = ci_workflows.assert_openclaw_profiles_render(repo_root, runner_temp)

expected_profiles = sorted(compatibility_helpers.PROFILES)
expected_home = (runner_temp / "strongclaw" / "nightly" / "profile-home").resolve()
expected_output_dir = (runner_temp / "strongclaw" / "nightly" / "openclaw-profiles").resolve()
assert rendered_profiles == expected_profiles
assert [profile for profile, _, _ in seen_calls] == expected_profiles
assert all(resolved_repo_root == repo_root.resolve() for _, resolved_repo_root, _ in seen_calls)
assert all(home_dir == expected_home for _, _, home_dir in seen_calls)
for profile_name in expected_profiles:
payload = json.loads(
(expected_output_dir / f"{profile_name}.json").read_text(encoding="utf-8")
)
assert payload == {"profile": profile_name}


def test_main_dispatches_assert_openclaw_profiles(
test_context: TestContext,
tmp_path: Path,
) -> None:
"""The CLI should dispatch all-profile rendering checks."""
seen_calls: list[tuple[Path, Path]] = []

def fake_assert_openclaw_profiles_render(repo_root: Path, runner_temp: Path) -> None:
seen_calls.append((repo_root, runner_temp))

test_context.patch.patch_object(
compatibility_matrix_script,
"assert_openclaw_profiles_render",
new=fake_assert_openclaw_profiles_render,
)

exit_code = compatibility_matrix_script.main(
[
"assert-openclaw-profiles",
"--repo-root",
str(tmp_path / "repo"),
"--runner-temp",
str(tmp_path / "runner-temp"),
]
)

assert exit_code == 0
assert seen_calls == [((tmp_path / "repo").resolve(), (tmp_path / "runner-temp").resolve())]
95 changes: 94 additions & 1 deletion tests/suites/unit/ci/test_fresh_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import argparse
import json
import subprocess
from collections.abc import Callable
from collections.abc import Callable, Mapping
from pathlib import Path
from typing import Any, cast

Expand Down Expand Up @@ -613,6 +613,99 @@ def fake_run(
)


def test_verify_compose_services_running_ignores_isolated_runtime_keys_from_local_env(
tmp_path: Path,
test_context: TestContext,
) -> None:
"""Compose probes should not accept legacy runtime-path keys when isolation is active."""
repo_root = tmp_path / "repo"
compose_dir = repo_root / "platform" / "compose"
compose_dir.mkdir(parents=True)
compose_file = compose_dir / "docker-compose.browser-lab.yaml"
compose_file.write_text("services: {}\n", encoding="utf-8")
home_dir = tmp_path / "home"
home_dir.mkdir()
runtime_root = home_dir / "runtime-root"
local_env_file = tmp_path / "legacy.env.local"
local_env_file.write_text(
"\n".join(
(
"OPENCLAW_STATE_DIR=/tmp/legacy-openclaw-state",
"OPENCLAW_CONFIG_PATH=/tmp/legacy-openclaw.json",
"OPENCLAW_PROFILE=legacy-profile",
"NEO4J_PASSWORD=repo-secret",
)
)
+ "\n",
encoding="utf-8",
)
captured_env: dict[str, str] = {}
payload = json.dumps(
[
{"Service": "browserlab-proxy", "State": "running"},
{"Service": "browserlab-playwright", "State": "running"},
]
)

def fake_run(
args: list[str],
*,
cwd: Path | None = None,
env: dict[str, str] | None = None,
check: bool = False,
timeout: float | None = None,
text: bool = False,
capture_output: bool = False,
) -> subprocess.CompletedProcess[str]:
del args, cwd, check, timeout, text, capture_output
assert env is not None
captured_env.update(env)
return subprocess.CompletedProcess(
args=["docker", "compose"],
returncode=0,
stdout=payload,
stderr="",
)

def _varlock_local_env_file(
_repo_root: Path,
*,
home_dir: Path | None = None,
environ: Mapping[str, str] | None = None,
) -> Path:
del home_dir, environ
return local_env_file

test_context.patch.patch_object(fresh_host_shell.subprocess, "run", new=fake_run)
test_context.patch.patch_object(
fresh_host_shell,
"varlock_local_env_file",
new=_varlock_local_env_file,
)

fresh_host_shell.verify_compose_services_running(
compose_file,
cwd=compose_dir,
env={
"HOME": str(home_dir),
"PATH": "/usr/bin",
"STRONGCLAW_RUNTIME_ROOT": str(runtime_root),
},
expected_services=("browserlab-proxy", "browserlab-playwright"),
repo_root_path=repo_root,
repo_local_state=True,
)

expected_state_dir = (runtime_root / ".openclaw").resolve()
expected_config_path = expected_state_dir / "openclaw.json"
assert captured_env["OPENCLAW_HOME"] == str(runtime_root.resolve())
assert captured_env["OPENCLAW_STATE_DIR"] == str(expected_state_dir)
assert captured_env["OPENCLAW_CONFIG_PATH"] == str(expected_config_path)
assert captured_env["OPENCLAW_CONFIG"] == str(expected_config_path)
assert captured_env["OPENCLAW_PROFILE"] == "strongclaw-dev"
assert captured_env["NEO4J_PASSWORD"] == "repo-secret"


def test_verify_compose_services_running_honors_repo_local_state_override_for_variants(
tmp_path: Path,
test_context: TestContext,
Expand Down
80 changes: 80 additions & 0 deletions tests/suites/unit/ci/test_memory_plugin_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,51 @@ def fake_urlopen(url: str, timeout: int) -> _Response:
assert sleeps == [1.5, 1.5]


def test_run_clawops_memory_migration_invokes_dry_run_cli(
test_context: TestContext,
tmp_path: Path,
) -> None:
"""Memory migration checks should execute the dry-run clawops migration CLI."""
repo_root = tmp_path / "repo"
repo_root.mkdir()
runner_temp = tmp_path / "runner-temp"
seen_calls: list[tuple[list[str], Path | None, dict[str, str] | None]] = []

def fake_run_checked(
command: list[str],
*,
cwd: Path | None = None,
env: dict[str, str] | None = None,
timeout_seconds: int | None = None,
capture_output: bool = False,
) -> Any:
del timeout_seconds, capture_output
seen_calls.append((command, cwd, env))
return None

test_context.patch.patch_object(memory_plugin_helpers, "run_checked", new=fake_run_checked)
test_context.env.remove("PYTHONPATH")

report_path = ci_workflows.run_clawops_memory_migration(repo_root, runner_temp=runner_temp)

assert report_path == runner_temp.resolve() / "clawops-memory-migration-report.json"
command, command_cwd, command_env = seen_calls[0]
assert command[:8] == [
"uv",
"run",
"python",
"-m",
"clawops",
"memory",
"migrate-hypermemory-to-pro",
"--dry-run",
]
assert command[-2:] == ["--report", str(report_path)]
assert command_cwd == repo_root.resolve()
assert command_env is not None
assert command_env["PYTHONPATH"] == "src"


def test_main_dispatches_wait_for_qdrant(test_context: TestContext) -> None:
"""The CLI should dispatch Qdrant readiness checks."""
seen_calls: list[tuple[str, int, float]] = []
Expand All @@ -107,3 +152,38 @@ def fake_wait_for_qdrant(url: str, *, attempts: int = 30, sleep_seconds: float =

assert exit_code == 0
assert seen_calls == [("http://127.0.0.1:6333/healthz", 12, 2.0)]


def test_main_dispatches_run_clawops_memory_migration(
test_context: TestContext,
tmp_path: Path,
) -> None:
"""The CLI should dispatch dry-run clawops memory migration checks."""
seen_calls: list[tuple[Path, Path | None]] = []

def fake_run_clawops_memory_migration(
repo_root: Path,
*,
runner_temp: Path | None = None,
) -> Path:
seen_calls.append((repo_root, runner_temp))
return tmp_path / "report.json"

test_context.patch.patch_object(
memory_plugin_script,
"run_clawops_memory_migration",
new=fake_run_clawops_memory_migration,
)

exit_code = memory_plugin_script.main(
[
"run-clawops-memory-migration",
"--repo-root",
str(tmp_path / "repo"),
"--runner-temp",
str(tmp_path / "runner-temp"),
]
)

assert exit_code == 0
assert seen_calls == [((tmp_path / "repo").resolve(), (tmp_path / "runner-temp").resolve())]
Loading
Loading