chore: import upstream snapshot with attribution
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:44 +08:00
commit bcbd1bdb22
5748 changed files with 562488 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
+84
View File
@@ -0,0 +1,84 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import os
import socket
import subprocess
import sys
import time
import httpx
import pytest
def _free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
def _wait_for_health(url: str, timeout: float = 8.0) -> None:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
r = httpx.get(f"{url}/v1/health", timeout=0.3)
if r.status_code == 200:
return
except httpx.RequestError:
pass
time.sleep(0.1)
raise RuntimeError(f"daemon at {url} did not start within {timeout}s")
@pytest.fixture
def daemon(tmp_path):
"""Spin up a real daemon subprocess for the test, on a free port."""
port = _free_port()
url = f"http://127.0.0.1:{port}"
env = dict(os.environ)
env["MIRAGE_DAEMON_URL"] = url
env["MIRAGE_IDLE_GRACE_SECONDS"] = "60"
env["MIRAGE_HOME"] = str(tmp_path)
env["MIRAGE_VERSION_ROOT"] = str(tmp_path / "repos")
env["MIRAGE_SNAPSHOT_ROOT"] = str(tmp_path)
env.pop("MIRAGE_AUTH_TOKEN", None)
env.pop("MIRAGE_TOKEN", None)
log_file = tmp_path / "daemon.log"
proc = subprocess.Popen(
[
sys.executable,
"-m",
"uvicorn",
"mirage.server.daemon:app",
"--host",
"127.0.0.1",
"--port",
str(port),
"--log-level",
"warning",
],
env=env,
stdout=open(log_file, "wb"),
stderr=subprocess.STDOUT,
)
try:
_wait_for_health(url)
yield {"url": url, "env": env, "log": log_file}
finally:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
+398
View File
@@ -0,0 +1,398 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import json
import subprocess
import sys
from pathlib import Path
CONFIG_YAML = """\
mounts:
/:
resource: ram
mode: WRITE
"""
def _write_config(tmp_path: Path) -> Path:
p = tmp_path / "config.yaml"
p.write_text(CONFIG_YAML, encoding="utf-8")
return p
def _run_cli(env: dict,
*args: str,
stdin: bytes | None = None,
expect_exit: int = 0) -> dict | list:
cmd = [sys.executable, "-m", "mirage.cli.main", *args]
proc = subprocess.run(
cmd,
env=env,
input=stdin,
capture_output=True,
timeout=30,
)
if proc.returncode != expect_exit:
raise AssertionError(
f"exit={proc.returncode} (expected {expect_exit})\n"
f"stdout: {proc.stdout.decode()}\nstderr: {proc.stderr.decode()}")
if expect_exit != 0:
return {}
if not proc.stdout.strip():
return {}
return json.loads(proc.stdout)
def test_workspace_lifecycle(daemon, tmp_path):
cfg = _write_config(tmp_path)
created = _run_cli(daemon["env"], "workspace", "create", str(cfg))
wid = created["id"]
assert wid.startswith("ws_")
listed = _run_cli(daemon["env"], "workspace", "list")
assert any(w["id"] == wid for w in listed)
detail = _run_cli(daemon["env"], "workspace", "get", wid)
assert detail["id"] == wid
deleted = _run_cli(daemon["env"], "workspace", "delete", wid)
assert deleted["id"] == wid
def test_workspace_create_with_explicit_id(daemon, tmp_path):
cfg = _write_config(tmp_path)
created = _run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"custom-ws")
assert created["id"] == "custom-ws"
_run_cli(daemon["env"], "workspace", "delete", "custom-ws")
def test_session_lifecycle(daemon, tmp_path):
cfg = _write_config(tmp_path)
created = _run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"sess-test-ws")
wid = created["id"]
sess = _run_cli(daemon["env"], "session", "create", wid, "--id", "agent_a")
assert sess["session_id"] == "agent_a"
listed = _run_cli(daemon["env"], "session", "list", wid)
assert any(s["session_id"] == "agent_a" for s in listed)
_run_cli(daemon["env"], "session", "delete", wid, "agent_a")
_run_cli(daemon["env"], "workspace", "delete", wid)
def test_execute_returns_json_io_result(daemon, tmp_path):
cfg = _write_config(tmp_path)
created = _run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"exec-test")
result = _run_cli(daemon["env"], "execute", "--workspace_id",
created["id"], "--command", "echo hello")
assert result["kind"] == "io"
assert result["exit_code"] == 0
assert result["stdout"].startswith("hello")
_run_cli(daemon["env"], "workspace", "delete", "exec-test")
def test_execute_background_returns_job_id(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id", "bg-test")
submitted = _run_cli(daemon["env"], "execute", "--workspace_id", "bg-test",
"--command", "echo bg", "--background")
job_id = submitted["job_id"]
assert job_id.startswith("job_")
waited = _run_cli(daemon["env"], "job", "wait", job_id)
assert waited["status"] == "done"
assert waited["result"]["stdout"].startswith("bg")
_run_cli(daemon["env"], "workspace", "delete", "bg-test")
def test_execute_with_stdin_pipe(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"stdin-test")
result = _run_cli(daemon["env"],
"execute",
"--workspace_id",
"stdin-test",
"--command",
"wc -l",
stdin=b"a\nb\nc\n")
assert result["exit_code"] == 0
assert result["stdout"].strip().startswith("3")
_run_cli(daemon["env"], "workspace", "delete", "stdin-test")
def test_save_then_load_round_trip(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"save-test")
_run_cli(daemon["env"], "execute", "--workspace_id", "save-test",
"--command", "echo persisted > /report.txt")
tar_path = tmp_path / "snap.tar"
saved = _run_cli(daemon["env"], "workspace", "snapshot", "save-test",
str(tar_path))
assert saved["size"] > 0
assert tar_path.exists()
loaded = _run_cli(daemon["env"], "workspace", "load", str(tar_path),
str(cfg), "--id", "loaded-ws")
assert loaded["id"] == "loaded-ws"
result = _run_cli(daemon["env"], "execute", "--workspace_id", "loaded-ws",
"--command", "cat /report.txt")
assert "persisted" in result["stdout"]
_run_cli(daemon["env"], "workspace", "delete", "save-test")
_run_cli(daemon["env"], "workspace", "delete", "loaded-ws")
def test_workspace_clone_round_trip(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"clone-src")
_run_cli(daemon["env"], "execute", "--workspace_id", "clone-src",
"--command", "echo source > /report.txt")
cloned = _run_cli(daemon["env"], "workspace", "clone", "clone-src", "--id",
"clone-dst")
assert cloned["id"] == "clone-dst"
result = _run_cli(daemon["env"], "execute", "--workspace_id", "clone-dst",
"--command", "cat /report.txt")
assert "source" in result["stdout"]
_run_cli(daemon["env"], "execute", "--workspace_id", "clone-dst",
"--command", "echo clone > /report.txt")
original = _run_cli(daemon["env"], "execute", "--workspace_id",
"clone-src", "--command", "cat /report.txt")
assert "source" in original["stdout"]
_run_cli(daemon["env"], "workspace", "delete", "clone-src")
_run_cli(daemon["env"], "workspace", "delete", "clone-dst")
def test_workspace_get_verbose_includes_internals(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"verbose-test")
plain = _run_cli(daemon["env"], "workspace", "get", "verbose-test")
assert plain["internals"] is None
verbose = _run_cli(daemon["env"], "workspace", "get", "verbose-test",
"--verbose")
assert verbose["internals"] is not None
assert "cache_bytes" in verbose["internals"]
_run_cli(daemon["env"], "workspace", "delete", "verbose-test")
def test_unknown_workspace_returns_nonzero(daemon, tmp_path):
_run_cli(daemon["env"],
"workspace",
"get",
"ws_doesnotexist",
expect_exit=2)
def test_env_interpolation_uses_cli_environment(daemon, tmp_path):
"""${VAR}s in the YAML must be resolved from the CLI's env, not the
daemon's. The cross-mount workflow assumes the user sources their
creds before running `mirage workspace create` -- the daemon
process won't have those vars."""
cfg = tmp_path / "interp.yaml"
cfg.write_text(
"mounts:\n"
" /:\n"
" resource: ram\n"
" mode: ${MOUNT_MODE_FROM_ENV}\n",
encoding="utf-8",
)
env = {**daemon["env"], "MOUNT_MODE_FROM_ENV": "WRITE"}
_run_cli(env, "workspace", "create", str(cfg), "--id", "interp-test")
detail = _run_cli(env, "workspace", "get", "interp-test")
assert any(m["mode"] == "write" for m in detail["mounts"])
_run_cli(env, "workspace", "delete", "interp-test")
def test_daemon_status_running(daemon, tmp_path):
out = _run_cli(daemon["env"], "daemon", "status")
assert out["running"] is True
assert out["pid"] is not None
assert out["workspaces"] == 0
def test_daemon_stop_then_status_not_running(daemon, tmp_path):
out = _run_cli(daemon["env"], "daemon", "stop")
assert out["stopped"] is True
import time
time.sleep(0.5)
out = _run_cli(daemon["env"], "daemon", "status", expect_exit=1)
assert out == {} or out.get("running") is False
def test_provision_returns_dry_run_result(daemon, tmp_path):
"""`mirage provision` hits /execute with provision=True and returns
a {"kind": "provision", ...} payload instead of actually running."""
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"provision-test")
result = _run_cli(
daemon["env"],
"provision",
"--workspace_id",
"provision-test",
"--command",
"echo would-not-run",
)
assert result["kind"] == "provision"
_run_cli(daemon["env"], "workspace", "delete", "provision-test")
def test_execute_propagates_inner_exit_code(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"exit-test")
ok = _run_cli(daemon["env"], "execute", "-w", "exit-test", "-c", "echo ok")
assert ok["exit_code"] == 0
_run_cli(daemon["env"],
"execute",
"-w",
"exit-test",
"-c",
"false",
expect_exit=1)
bg = _run_cli(daemon["env"], "execute", "-w", "exit-test", "-c", "false",
"--background")
job_id = bg["job_id"]
_run_cli(daemon["env"], "job", "wait", job_id, expect_exit=1)
_run_cli(daemon["env"], "workspace", "delete", "exit-test")
def test_execute_subshell_cwd_does_not_leak(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"subshell")
_run_cli(daemon["env"], "execute", "-w", "subshell", "-c", "mkdir /sub")
inside = _run_cli(daemon["env"], "execute", "-w", "subshell", "-c",
"(cd /sub && pwd)")
assert inside["stdout"].strip() == "/sub"
after = _run_cli(daemon["env"], "execute", "-w", "subshell", "-c", "pwd")
assert after["stdout"].strip() == "/"
_run_cli(daemon["env"], "workspace", "delete", "subshell")
def test_execute_env_prefix_does_not_leak(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id", "envpref")
inside = _run_cli(daemon["env"], "execute", "-w", "envpref", "-c",
"(export FOO=bar; printenv FOO)")
assert inside["stdout"].strip() == "bar"
after = _run_cli(daemon["env"], "execute", "-w", "envpref", "-c",
"printenv FOO || echo absent")
assert after["stdout"].strip() == "absent"
_run_cli(daemon["env"], "workspace", "delete", "envpref")
def test_execute_background_then_cancel(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"cancel-test")
submitted = _run_cli(daemon["env"], "execute", "-w", "cancel-test", "-c",
"sleep 30", "--background")
job_id = submitted["job_id"]
_run_cli(daemon["env"], "job", "cancel", job_id)
waited = _run_cli(daemon["env"], "job", "wait", job_id, expect_exit=2)
assert waited == {}
_run_cli(daemon["env"], "workspace", "delete", "cancel-test")
def test_missing_env_var_fails_fast_before_daemon_call(daemon, tmp_path):
cfg = tmp_path / "missing.yaml"
cfg.write_text(
"mounts:\n"
" /:\n"
" resource: ram\n"
" mode: ${THIS_VAR_IS_NOT_SET_ANYWHERE}\n",
encoding="utf-8",
)
env = {
k: v
for k, v in daemon["env"].items()
if k != "THIS_VAR_IS_NOT_SET_ANYWHERE"
}
_run_cli(env, "workspace", "create", str(cfg), expect_exit=2)
SAFEGUARD_TRUNCATE_YAML = """\
mounts:
/:
resource: ram
mode: WRITE
command_safeguards:
cat:
max_lines: 2
on_exceed: truncate
"""
SAFEGUARD_ERROR_YAML = """\
mounts:
/:
resource: ram
mode: WRITE
command_safeguards:
cat:
max_lines: 2
on_exceed: error
"""
_SEED_5_LINES = "printf '1\\n2\\n3\\n4\\n5\\n' > /f.txt"
def _write_named(tmp_path: Path, name: str, text: str) -> Path:
p = tmp_path / name
p.write_text(text, encoding="utf-8")
return p
def test_execute_safeguard_truncates_output(daemon, tmp_path):
cfg = _write_named(tmp_path, "sg_trunc.yaml", SAFEGUARD_TRUNCATE_YAML)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"sg-trunc")
_run_cli(daemon["env"], "execute", "--workspace_id", "sg-trunc",
"--command", _SEED_5_LINES)
result = _run_cli(daemon["env"], "execute", "--workspace_id", "sg-trunc",
"--command", "cat /f.txt")
assert result["exit_code"] == 0
assert result["stdout"] == "1\n2\n"
_run_cli(daemon["env"], "workspace", "delete", "sg-trunc")
def test_execute_safeguard_error_exits_1(daemon, tmp_path):
cfg = _write_named(tmp_path, "sg_err.yaml", SAFEGUARD_ERROR_YAML)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id", "sg-err")
_run_cli(daemon["env"], "execute", "--workspace_id", "sg-err", "--command",
_SEED_5_LINES)
_run_cli(daemon["env"],
"execute",
"--workspace_id",
"sg-err",
"--command",
"cat /f.txt",
expect_exit=1)
_run_cli(daemon["env"], "workspace", "delete", "sg-err")
+105
View File
@@ -0,0 +1,105 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.cli.client import DaemonClient
from mirage.cli.settings import DaemonSettings
from mirage.server.daemon_config import DaemonConfigError
from mirage.server.env import ENV_HOME
def test_spawn_daemon_uses_mirage_home_for_log_and_token(
tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv("MIRAGE_AUTH_MODE", raising=False)
spawned = []
monkeypatch.setattr("mirage.cli.client.subprocess.Popen",
lambda *args, **kwargs: spawned.append(kwargs))
with DaemonClient(DaemonSettings()) as client:
client._spawn_daemon()
assert spawned, "daemon process must be spawned"
assert (tmp_path / "daemon.log").exists()
assert (tmp_path / "auth_token").exists()
assert client.settings.auth_token
def test_spawn_daemon_rejects_bad_config(tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv("MIRAGE_AUTH_MODE", raising=False)
(tmp_path / "config.toml").write_text('[daemon]\ntypo_key = "x"\n')
spawned = []
monkeypatch.setattr("mirage.cli.client.subprocess.Popen",
lambda *args, **kwargs: spawned.append(kwargs))
with DaemonClient(DaemonSettings()) as client:
with pytest.raises(DaemonConfigError, match="typo_key"):
client._spawn_daemon()
assert not spawned
class _FakePopen:
def __init__(self, sink, cmd, **kwargs):
sink.append(cmd)
def test_spawn_port_config_beats_url(tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv("MIRAGE_AUTH_MODE", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_PORT", raising=False)
(tmp_path / "config.toml").write_text("[daemon]\nport = 9100\n")
cmds = []
monkeypatch.setattr("mirage.cli.client.subprocess.Popen",
lambda cmd, **kwargs: _FakePopen(cmds, cmd, **kwargs))
with DaemonClient(DaemonSettings()) as client:
client._spawn_daemon()
assert "9100" in cmds[0]
def test_spawn_port_env_beats_config(tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv("MIRAGE_AUTH_MODE", raising=False)
monkeypatch.setenv("MIRAGE_DAEMON_PORT", "9200")
(tmp_path / "config.toml").write_text("[daemon]\nport = 9100\n")
cmds = []
monkeypatch.setattr("mirage.cli.client.subprocess.Popen",
lambda cmd, **kwargs: _FakePopen(cmds, cmd, **kwargs))
with DaemonClient(DaemonSettings()) as client:
client._spawn_daemon()
assert "9200" in cmds[0]
def test_spawn_port_falls_back_to_url(tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv("MIRAGE_AUTH_MODE", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_PORT", raising=False)
cmds = []
monkeypatch.setattr("mirage.cli.client.subprocess.Popen",
lambda cmd, **kwargs: _FakePopen(cmds, cmd, **kwargs))
with DaemonClient(DaemonSettings(url="http://127.0.0.1:9331")) as client:
client._spawn_daemon()
assert "9331" in cmds[0]
def test_spawn_respects_config_auth_mode(tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv("MIRAGE_AUTH_MODE", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_PORT", raising=False)
(tmp_path / "config.toml").write_text('[daemon]\nauth_mode = "token"\n')
spawned = []
monkeypatch.setattr("mirage.cli.client.subprocess.Popen",
lambda *args, **kwargs: spawned.append(kwargs))
with DaemonClient(DaemonSettings()) as client:
client._spawn_daemon()
assert "MIRAGE_AUTH_MODE" not in spawned[0]["env"]
+105
View File
@@ -0,0 +1,105 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from typer.testing import CliRunner
from mirage.cli.config import app
runner = CliRunner()
def _isolate_home(monkeypatch, tmp_path):
monkeypatch.setenv("MIRAGE_HOME", str(tmp_path))
monkeypatch.delenv("MIRAGE_PID_FILE", raising=False)
def test_config_set_then_get(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
r = runner.invoke(app, ["set", "version_root", "/data/repos"])
assert r.exit_code == 0
r = runner.invoke(app, ["get", "version_root"])
assert r.exit_code == 0
assert "/data/repos" in r.stdout
def test_config_list(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
runner.invoke(app, ["set", "url", "http://a:1"])
r = runner.invoke(app, ["list"])
assert r.exit_code == 0
assert "url" in r.stdout and "http://a:1" in r.stdout
def test_config_unset(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
runner.invoke(app, ["set", "socket", "/tmp/s.sock"])
runner.invoke(app, ["unset", "socket"])
r = runner.invoke(app, ["get", "socket"])
assert r.exit_code != 0
def test_config_get_unset_exits_nonzero(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
r = runner.invoke(app, ["get", "pid_file"])
assert r.exit_code != 0
def test_config_set_rejects_unknown_key(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
r = runner.invoke(app, ["set", "MIRAGE_HOME", "/x"])
assert r.exit_code == 2
def test_config_set_unknown_key_message_is_clean(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
r = runner.invoke(app, ["set", "nope", "/x"])
assert r.exit_code == 2
assert "unknown config key" in r.output
assert not r.output.strip().startswith("'")
def test_config_list_malformed_toml_fails_cleanly(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
(tmp_path / "config.toml").write_text("[daemon\nnot toml")
r = runner.invoke(app, ["list"])
assert r.exit_code == 2
assert "malformed" in r.output
assert "Traceback" not in r.output
def test_config_list_warns_on_unknown_keys(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
(tmp_path / "config.toml").write_text('[daemon]\ntypo_key = "x"\n')
r = runner.invoke(app, ["list"])
assert r.exit_code == 0
assert "typo_key" in r.output
assert "unknown" in r.output.lower()
def test_config_list_resolved_shows_origin(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
monkeypatch.setenv("MIRAGE_VERSION_ROOT", "/env/repos")
r = runner.invoke(app, ["list", "--resolved"])
assert r.exit_code == 0
assert "/env/repos" in r.output
assert "MIRAGE_VERSION_ROOT" in r.output
def test_config_list_resolved_masks_auth_token(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
monkeypatch.setenv("MIRAGE_TOKEN", "supersecret")
r = runner.invoke(app, ["list", "--resolved"])
assert r.exit_code == 0
assert "supersecret" not in r.output
assert "***" in r.output
+160
View File
@@ -0,0 +1,160 @@
from mirage.cli.output import exit_code_from_response
def test_io_zero():
assert exit_code_from_response({
"kind": "io",
"exit_code": 0,
"stdout": "",
"stderr": ""
}) == 0
def test_io_nonzero():
assert exit_code_from_response({
"kind": "io",
"exit_code": 1,
"stdout": "",
"stderr": ""
}) == 1
assert exit_code_from_response({
"kind": "io",
"exit_code": 42,
"stdout": "",
"stderr": ""
}) == 42
assert exit_code_from_response({
"kind": "io",
"exit_code": 127,
"stdout": "",
"stderr": ""
}) == 127
def test_clamp_high():
assert exit_code_from_response({"kind": "io", "exit_code": 300}) == 255
def test_clamp_negative():
assert exit_code_from_response({"kind": "io", "exit_code": -1}) == 0
def test_truncate_float():
assert exit_code_from_response({"kind": "io", "exit_code": 1.9}) == 1
def test_nan_returns_zero():
assert exit_code_from_response({
"kind": "io",
"exit_code": float("nan")
}) == 0
def test_inf_returns_zero():
assert exit_code_from_response({
"kind": "io",
"exit_code": float("inf")
}) == 0
def test_bool_returns_zero():
assert exit_code_from_response({"kind": "io", "exit_code": True}) == 0
def test_bg_submission():
assert exit_code_from_response({
"job_id": "job_abc",
"workspace_id": "ws",
"submitted_at": 0,
}) == 0
def test_provision_kind():
assert exit_code_from_response({"kind": "provision", "detail": "ok"}) == 0
def test_raw_kind():
assert exit_code_from_response({"kind": "raw", "value": "hi"}) == 0
def test_job_detail_done():
assert exit_code_from_response({
"job_id": "job_x",
"status": "done",
"result": {
"kind": "io",
"exit_code": 7,
"stdout": "",
"stderr": ""
},
"error": None,
}) == 7
def test_job_pending_returns_zero():
assert exit_code_from_response({
"job_id": "job_x",
"status": "pending",
"result": None,
"error": None,
}) == 0
def test_job_running_returns_zero():
assert exit_code_from_response({
"job_id": "job_x",
"status": "running",
"result": None,
"error": None,
}) == 0
def test_job_failed_no_result_returns_two():
assert exit_code_from_response({
"job_id": "job_x",
"status": "failed",
"result": None,
"error": "boom",
}) == 2
def test_job_canceled_no_result_returns_two():
assert exit_code_from_response({
"job_id": "job_x",
"status": "canceled",
"result": None,
"error": None,
}) == 2
def test_job_failed_with_result_prefers_inner():
assert exit_code_from_response({
"job_id": "job_x",
"status": "failed",
"result": {
"kind": "io",
"exit_code": 9,
"stdout": "",
"stderr": ""
},
"error": None,
}) == 9
def test_non_dict_inputs():
assert exit_code_from_response(None) == 0
assert exit_code_from_response("string") == 0
assert exit_code_from_response(42) == 0
assert exit_code_from_response([1, 2, 3]) == 0
def test_io_missing_exit_code():
assert exit_code_from_response({
"kind": "io",
"stdout": "",
"stderr": ""
}) == 0
def test_io_non_numeric_exit_code():
assert exit_code_from_response({"kind": "io", "exit_code": "one"}) == 0
+102
View File
@@ -0,0 +1,102 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import re
from contextlib import contextmanager
from typer.testing import CliRunner
from mirage.cli import session as session_cli
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
class _FakeResponse:
status_code = 201
content = b'{"session_id": "agent", "cwd": "/"}'
def json(self) -> dict:
return {"session_id": "agent", "cwd": "/"}
class _FakeClient:
def __init__(self) -> None:
self.last_body: dict | None = None
def __enter__(self):
return self
def __exit__(self, *_):
return False
def ensure_running(self, allow_spawn: bool = False) -> None:
return None
def request(self, method: str, path: str, json: dict | None = None):
self.last_body = json
return _FakeResponse()
@contextmanager
def _patched_client(fake: _FakeClient):
real = session_cli.make_client
session_cli.make_client = lambda: fake
try:
yield fake
finally:
session_cli.make_client = real
def test_session_create_passes_mount_flags():
fake = _FakeClient()
with _patched_client(fake):
result = CliRunner().invoke(
session_cli.app,
[
"create", "demo", "--id", "agent", "-m", "/s3", "--mount",
"/slack"
],
)
assert result.exit_code == 0, result.output
assert fake.last_body == {
"session_id": "agent",
"allowed_mounts": ["/s3", "/slack"],
}
def test_session_create_no_mount_flag_omits_field():
fake = _FakeClient()
with _patched_client(fake):
result = CliRunner().invoke(
session_cli.app,
["create", "demo", "--id", "agent"],
)
assert result.exit_code == 0, result.output
assert fake.last_body == {"session_id": "agent"}
def test_session_create_without_id_or_mount_sends_empty_body():
fake = _FakeClient()
with _patched_client(fake):
result = CliRunner().invoke(session_cli.app, ["create", "demo"])
assert result.exit_code == 0, result.output
assert fake.last_body == {}
def test_session_create_help_lists_mount_flag():
result = CliRunner().invoke(session_cli.app, ["create", "--help"])
assert result.exit_code == 0
plain = _ANSI_RE.sub("", result.output)
assert "--mount" in plain
+203
View File
@@ -0,0 +1,203 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.cli.settings import (DEFAULT_DAEMON_URL, config_path, get_config,
list_config, load_daemon_settings,
resolved_config, set_config, unset_config)
from mirage.server.daemon_config import DaemonConfigError
from mirage.server.env import ENV_HOME
def test_load_daemon_settings_falls_back_to_token_file(tmp_path, monkeypatch):
monkeypatch.delenv("MIRAGE_TOKEN", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_URL", raising=False)
monkeypatch.setenv(ENV_HOME, str(tmp_path))
config_file = tmp_path / "config.toml"
config_file.write_text("")
token_file = tmp_path / "auth_token"
token_file.write_text("file-token\n")
settings = load_daemon_settings(path=config_file)
assert settings.auth_token == "file-token"
def test_load_daemon_settings_env_wins_over_file(tmp_path, monkeypatch):
monkeypatch.setenv("MIRAGE_TOKEN", "from-env")
monkeypatch.delenv("MIRAGE_DAEMON_URL", raising=False)
monkeypatch.setenv(ENV_HOME, str(tmp_path))
config_file = tmp_path / "config.toml"
config_file.write_text("")
token_file = tmp_path / "auth_token"
token_file.write_text("from-file")
settings = load_daemon_settings(path=config_file)
assert settings.auth_token == "from-env"
def test_load_daemon_settings_config_wins_over_file(tmp_path, monkeypatch):
monkeypatch.delenv("MIRAGE_TOKEN", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_URL", raising=False)
monkeypatch.setenv(ENV_HOME, str(tmp_path))
config_file = tmp_path / "config.toml"
config_file.write_text('[daemon]\nauth_token = "from-config"\n')
token_file = tmp_path / "auth_token"
token_file.write_text("from-file")
settings = load_daemon_settings(path=config_file)
assert settings.auth_token == "from-config"
def test_load_daemon_settings_no_sources_yields_empty_token(
tmp_path, monkeypatch):
monkeypatch.delenv("MIRAGE_TOKEN", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_URL", raising=False)
monkeypatch.setenv(ENV_HOME, str(tmp_path))
config_file = tmp_path / "config.toml"
config_file.write_text("")
settings = load_daemon_settings(path=config_file)
assert settings.auth_token == ""
def test_config_path_follows_mirage_home(tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
assert config_path() == tmp_path / "config.toml"
def test_load_daemon_settings_reads_config_under_mirage_home(
tmp_path, monkeypatch):
monkeypatch.delenv("MIRAGE_TOKEN", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_URL", raising=False)
monkeypatch.setenv(ENV_HOME, str(tmp_path))
(tmp_path /
"config.toml").write_text('[daemon]\nurl = "http://127.0.0.1:9999"\n')
settings = load_daemon_settings()
assert settings.url == "http://127.0.0.1:9999"
def test_set_config_creates_file(tmp_path):
p = tmp_path / "config.toml"
set_config("version_root", "/data/repos", path=p)
assert get_config("version_root", path=p) == "/data/repos"
assert '[daemon]' in p.read_text()
def test_set_config_updates_existing_key(tmp_path):
p = tmp_path / "config.toml"
set_config("url", "http://a:1", path=p)
set_config("url", "http://b:2", path=p)
assert get_config("url", path=p) == "http://b:2"
assert p.read_text().count("url =") == 1
def test_set_config_preserves_comments_and_other_keys(tmp_path):
p = tmp_path / "config.toml"
p.write_text('[daemon]\n# keep me\nurl = "http://a:1"\n')
set_config("pid_file", "/tmp/x.pid", path=p)
text = p.read_text()
assert "# keep me" in text
assert 'url = "http://a:1"' in text
assert get_config("pid_file", path=p) == "/tmp/x.pid"
def test_unset_config_removes_key(tmp_path):
p = tmp_path / "config.toml"
set_config("socket", "/tmp/s.sock", path=p)
unset_config("socket", path=p)
assert get_config("socket", path=p) is None
def test_list_config_returns_written_keys(tmp_path):
p = tmp_path / "config.toml"
set_config("url", "http://a:1", path=p)
set_config("snapshot_root", "/snaps", path=p)
assert list_config(path=p) == {
"url": "http://a:1",
"snapshot_root": "/snaps"
}
def test_set_config_rejects_unknown_key(tmp_path):
with pytest.raises(DaemonConfigError, match="unknown config key"):
set_config("MIRAGE_HOME", "/x", path=tmp_path / "config.toml")
def test_numeric_key_written_bare(tmp_path):
p = tmp_path / "config.toml"
set_config("idle_grace_seconds", "45", path=p)
assert "idle_grace_seconds = 45" in p.read_text()
def test_unset_config_accepts_unknown_key(tmp_path):
p = tmp_path / "config.toml"
p.write_text('[daemon]\ntypo_key = "x"\nurl = "http://a:1"\n')
unset_config("typo_key", path=p)
assert "typo_key" not in p.read_text()
assert 'url = "http://a:1"' in p.read_text()
def test_set_config_chmods_0600(tmp_path):
p = tmp_path / "config.toml"
set_config("auth_token", "s3cret", path=p)
assert (p.stat().st_mode & 0o777) == 0o600
def test_unset_config_chmods_0600(tmp_path):
p = tmp_path / "config.toml"
p.write_text('[daemon]\nurl = "http://a:1"\nsocket = "/tmp/s"\n')
unset_config("socket", path=p)
assert (p.stat().st_mode & 0o777) == 0o600
def test_load_daemon_settings_missing_explicit_path_returns_defaults(
tmp_path, monkeypatch):
monkeypatch.delenv("MIRAGE_TOKEN", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_URL", raising=False)
monkeypatch.setenv(ENV_HOME, str(tmp_path))
settings = load_daemon_settings(path=tmp_path / "nope.toml")
assert settings.url == DEFAULT_DAEMON_URL
def test_resolved_config_reports_origins(monkeypatch, tmp_path):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.setenv("MIRAGE_VERSION_ROOT", "/env/repos")
monkeypatch.delenv("MIRAGE_SNAPSHOT_ROOT", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_URL", raising=False)
(tmp_path / "config.toml").write_text(
'[daemon]\nversion_root = "/file/repos"\nurl = "http://f:1"\n')
resolved = resolved_config()
assert resolved["version_root"] == ("/env/repos",
"env MIRAGE_VERSION_ROOT")
assert resolved["url"] == ("http://f:1", "file")
assert resolved["snapshot_root"] == (str(tmp_path / "snapshots"),
"default")
def test_resolved_config_defaults_when_nothing_set(monkeypatch, tmp_path):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
for name in ("MIRAGE_DAEMON_URL", "MIRAGE_TOKEN", "MIRAGE_PID_FILE",
"MIRAGE_VERSION_ROOT", "MIRAGE_SNAPSHOT_ROOT",
"MIRAGE_IDLE_GRACE_SECONDS"):
monkeypatch.delenv(name, raising=False)
resolved = resolved_config()
assert resolved["url"] == (DEFAULT_DAEMON_URL, "default")
assert resolved["pid_file"] == (str(tmp_path / "daemon.pid"), "default")
assert resolved["idle_grace_seconds"] == ("30", "default")
def test_resolved_config_includes_port(monkeypatch, tmp_path):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv("MIRAGE_DAEMON_PORT", raising=False)
resolved = resolved_config()
assert resolved["port"] == ("8765", "default")
monkeypatch.setenv("MIRAGE_DAEMON_PORT", "9100")
assert resolved_config()["port"] == ("9100", "env MIRAGE_DAEMON_PORT")
+202
View File
@@ -0,0 +1,202 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import json
import subprocess
import sys
from pathlib import Path
CONFIG_YAML = """\
mounts:
/:
resource: ram
mode: WRITE
"""
def _write_config(tmp_path: Path) -> Path:
p = tmp_path / "config.yaml"
p.write_text(CONFIG_YAML, encoding="utf-8")
return p
def _run(env: dict, *args: str, expect_exit: int = 0) -> dict | list:
cmd = [sys.executable, "-m", "mirage.cli.main", *args]
proc = subprocess.run(cmd, env=env, capture_output=True, timeout=30)
if proc.returncode != expect_exit:
raise AssertionError(
f"exit={proc.returncode} (expected {expect_exit})\n"
f"stdout: {proc.stdout.decode()}\nstderr: {proc.stderr.decode()}")
if expect_exit != 0 or not proc.stdout.strip():
return {}
return json.loads(proc.stdout)
def test_commit_log_checkout_clone(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo v1 > /notes.txt")
v1 = _run(env, "workspace", "commit", wid, "-m", "first")["version"]
_run(env, "execute", "-w", wid, "-c", "echo v2 > /notes.txt")
_run(env, "workspace", "commit", wid, "-m", "second")
log = _run(env, "workspace", "log", wid)
assert [e["message"] for e in log] == ["second", "first"]
_run(env, "workspace", "checkout", wid, v1)
reverted = _run(env, "execute", "-w", wid, "-c", "cat /notes.txt")
assert reverted["stdout"] == "v1\n"
clone = _run(env, "workspace", "clone", wid, "--at", v1)
assert clone["id"] != wid
def test_log_empty_before_commit(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
assert _run(env, "workspace", "log", wid) == []
def test_diff_versions_and_live(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo one > /a.txt")
v1 = _run(env, "workspace", "commit", wid, "-m", "first")["version"]
_run(env, "execute", "-w", wid, "-c", "echo two > /a.txt")
_run(env, "execute", "-w", wid, "-c", "echo new > /b.txt")
v2 = _run(env, "workspace", "commit", wid, "-m", "second")["version"]
by_version = _run(env, "workspace", "diff", wid, v1, v2)
assert by_version["modified"] == ["a.txt"]
assert by_version["added"] == ["b.txt"]
_run(env, "execute", "-w", wid, "-c", "echo three > /a.txt")
live = _run(env, "workspace", "diff", wid)
assert live["modified"] == ["a.txt"]
def test_branch_diverges_and_guards_commit(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo one > /a.txt")
_run(env, "workspace", "commit", wid, "-m", "first")
_run(env, "workspace", "branch", wid, "exp")
_run(env, "execute", "-w", wid, "-c", "echo two > /a.txt")
_run(env, "workspace", "commit", wid, "-b", "exp", "-m", "on exp")
exp_log = _run(env, "workspace", "log", wid, "-b", "exp")
main_log = _run(env, "workspace", "log", wid, "-b", "main")
assert [e["message"] for e in exp_log] == ["on exp", "first"]
assert [e["message"] for e in main_log] == ["first"]
_run(env,
"workspace",
"commit",
wid,
"-b",
"ghost",
"-m",
"x",
expect_exit=2)
def test_diff_includes_deleted(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo one > /a.txt")
_run(env, "execute", "-w", wid, "-c", "echo two > /b.txt")
v1 = _run(env, "workspace", "commit", wid, "-m", "first")["version"]
_run(env, "execute", "-w", wid, "-c", "rm /b.txt")
v2 = _run(env, "workspace", "commit", wid, "-m", "second")["version"]
changes = _run(env, "workspace", "diff", wid, v1, v2)
assert changes["deleted"] == ["b.txt"]
def test_clone_live_and_explicit_id(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo hello > /a.txt")
auto = _run(env, "workspace", "clone", wid)
assert auto["id"] != wid
named = _run(env, "workspace", "clone", wid, "--id", "myclone")
assert named["id"] == "myclone"
got = _run(env, "execute", "-w", "myclone", "-c", "cat /a.txt")
assert got["stdout"] == "hello\n"
def test_branch_from_non_main(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo one > /a.txt")
_run(env, "workspace", "commit", wid, "-m", "first")
_run(env, "workspace", "branch", wid, "exp")
_run(env, "execute", "-w", wid, "-c", "echo two > /a.txt")
_run(env, "workspace", "commit", wid, "-b", "exp", "-m", "on exp")
_run(env, "workspace", "branch", wid, "exp2", "--from", "exp")
log = _run(env, "workspace", "log", wid, "-b", "exp2")
assert [e["message"] for e in log] == ["on exp", "first"]
def test_checkout_by_branch_name(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo one > /a.txt")
_run(env, "workspace", "commit", wid, "-m", "first")
_run(env, "workspace", "branch", wid, "exp")
_run(env, "execute", "-w", wid, "-c", "echo two > /a.txt")
_run(env, "workspace", "commit", wid, "-b", "exp", "-m", "on exp")
_run(env, "workspace", "checkout", wid, "main")
on_main = _run(env, "execute", "-w", wid, "-c", "cat /a.txt")
assert on_main["stdout"] == "one\n"
_run(env, "workspace", "checkout", wid, "exp")
on_exp = _run(env, "execute", "-w", wid, "-c", "cat /a.txt")
assert on_exp["stdout"] == "two\n"
def test_error_paths_exit_2(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo one > /a.txt")
_run(env, "workspace", "commit", wid, "-m", "first")
_run(env, "workspace", "branch", wid, "exp")
_run(env, "workspace", "branch", wid, "exp", expect_exit=2)
_run(env, "workspace", "commit", "ghost-ws", "-m", "x", expect_exit=2)
_run(env, "workspace", "checkout", wid, "nope", expect_exit=2)
_run(env, "workspace", "diff", wid, "nope", "nope2", expect_exit=2)