mirror of
https://github.com/volcengine/OpenViking.git
synced 2026-09-29 04:02:57 +08:00
* refactor: share vector URI rewrite rules * feat: add strict vector transfer transactions * feat: add transactional VikingFS copy * feat: coordinate filesystem copy service * feat: expose filesystem copy API * feat: add ov cp command * test: cover filesystem copy end to end * test: compose CLI collection hooks * fix: support vector copy on volcengine backend * fix: harden copy transaction boundaries * fix: retain copied entry in parent semantics * fix: scan local vectors with supported sort key * fix: refresh move target parent semantics * fix: report missing copy target directory * feat: rebuild transfer parent semantics from target summaries * fix: refresh both parents after move * docs: document filesystem copy API * fix: adapt copy flow to canonical URI boundary * fix: adapt transfer semantics after upstream rebase * fix: harden copy and move transaction boundaries * fix: preserve transfer metadata and direct ACLs * ci: exercise copy with available capabilities * fix: restore move vectors after ACL refresh failure --------- Co-authored-by: chenpengfei <chenpengfei@bytedance.com>
680 lines
21 KiB
Python
680 lines
21 KiB
Python
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
|
|
# SPDX-License-Identifier: AGPL-3.0
|
|
"""CLI fixtures that run against a real OpenViking server process."""
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import Generator
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from openviking_cli.utils.config import OPENVIKING_CONFIG_ENV
|
|
|
|
|
|
def _get_free_port() -> int:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.bind(("127.0.0.1", 0))
|
|
port = sock.getsockname()[1]
|
|
sock.close()
|
|
return port
|
|
|
|
|
|
def _wait_for_health(url: str, timeout_s: float = 20.0) -> None:
|
|
deadline = time.time() + timeout_s
|
|
last_error = None
|
|
while time.time() < deadline:
|
|
try:
|
|
response = httpx.get(f"{url}/health", timeout=1.0)
|
|
if response.status_code == 200:
|
|
return
|
|
except Exception as exc: # noqa: BLE001
|
|
last_error = exc
|
|
time.sleep(0.25)
|
|
raise RuntimeError(f"OpenViking server failed to start: {last_error}")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def openviking_server(tmp_path_factory: pytest.TempPathFactory) -> Generator[str, None, None]:
|
|
storage_dir = tmp_path_factory.mktemp("openviking_cli_data")
|
|
port = _get_free_port()
|
|
|
|
base_conf_path = Path("examples/ov.conf").resolve()
|
|
with open(base_conf_path) as f:
|
|
conf_data = json.load(f)
|
|
|
|
conf_data.setdefault("server", {})
|
|
conf_data["server"]["host"] = "127.0.0.1"
|
|
conf_data["server"]["port"] = port
|
|
|
|
conf_data.setdefault("storage", {})
|
|
conf_data["storage"]["workspace"] = str(storage_dir)
|
|
conf_data["storage"].setdefault("vectordb", {})
|
|
conf_data["storage"]["vectordb"]["backend"] = "local"
|
|
conf_data["storage"].setdefault("agfs", {})
|
|
conf_data["storage"]["agfs"]["backend"] = "local"
|
|
|
|
tmp_conf = storage_dir / "ov.conf"
|
|
with open(tmp_conf, "w") as f:
|
|
json.dump(conf_data, f)
|
|
|
|
env = os.environ.copy()
|
|
env[OPENVIKING_CONFIG_ENV] = str(tmp_conf)
|
|
|
|
cmd = [
|
|
sys.executable,
|
|
"-m",
|
|
"openviking",
|
|
"serve",
|
|
"--config",
|
|
str(tmp_conf),
|
|
]
|
|
|
|
proc = subprocess.Popen(
|
|
cmd,
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
|
|
url = f"http://127.0.0.1:{port}"
|
|
|
|
try:
|
|
_wait_for_health(url)
|
|
yield url
|
|
except RuntimeError:
|
|
stdout, stderr = "", ""
|
|
if proc.poll() is not None:
|
|
stdout, stderr = proc.communicate(timeout=5)
|
|
else:
|
|
proc.terminate()
|
|
stdout, stderr = proc.communicate(timeout=10)
|
|
raise RuntimeError(
|
|
f"OpenViking server failed to start.\nstdout:\n{stdout}\nstderr:\n{stderr}"
|
|
)
|
|
finally:
|
|
if proc.poll() is None:
|
|
proc.terminate()
|
|
try:
|
|
proc.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
proc.wait(timeout=10)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Remote CLI test infrastructure
|
|
# ---------------------------------------------------------------------------
|
|
|
|
OPENVIKING_BIN = os.getenv("OPENVIKING_CLI_BIN", "")
|
|
BASE_URL = os.getenv(
|
|
"OPENVIKING_URL",
|
|
os.getenv("SERVER_URL", "https://api.vikingdb.cn-beijing.volces.com/openviking"),
|
|
)
|
|
CONFIGURED_API_KEY = os.getenv("OPENVIKING_API_KEY", "")
|
|
ROOT_API_KEY = os.getenv("OPENVIKING_ROOT_API_KEY", "")
|
|
TRUSTED_IDENTITY_HEADERS = os.getenv("OPENVIKING_TRUSTED_IDENTITY_HEADERS", "").lower() in (
|
|
"1",
|
|
"true",
|
|
"yes",
|
|
)
|
|
CLI_ACCOUNT = os.getenv("OPENVIKING_ACCOUNT", "") if TRUSTED_IDENTITY_HEADERS else ""
|
|
CLI_USER = os.getenv("OPENVIKING_USER", "") if TRUSTED_IDENTITY_HEADERS else ""
|
|
|
|
|
|
def _admin_headers() -> dict[str, str]:
|
|
if not ROOT_API_KEY:
|
|
return {}
|
|
return {"Authorization": f"Bearer {ROOT_API_KEY}"}
|
|
|
|
|
|
def _find_user(users: list[object], user_id: str) -> dict[str, object] | None:
|
|
for user in users:
|
|
if isinstance(user, dict) and user.get("user_id") == user_id:
|
|
return user
|
|
return None
|
|
|
|
|
|
def _register_regular_user(account_id: str, user_id: str) -> str:
|
|
register_resp = httpx.post(
|
|
f"{BASE_URL}/api/v1/admin/accounts/{account_id}/users",
|
|
headers=_admin_headers(),
|
|
json={"user_id": user_id, "role": "user"},
|
|
timeout=10.0,
|
|
)
|
|
if register_resp.status_code in (200, 201):
|
|
user_key = register_resp.json().get("result", {}).get("user_key")
|
|
if isinstance(user_key, str) and user_key:
|
|
return user_key
|
|
return ""
|
|
|
|
|
|
def _resolve_api_key() -> str:
|
|
"""Resolve a user API key suitable for data-plane operations.
|
|
|
|
Root keys (OPENVIKING_API_KEY/OPENVIKING_ROOT_API_KEY) cannot be used for
|
|
data-plane operations like add-skill/ls/read. When only a root key is
|
|
available we need to provision a regular user via admin APIs.
|
|
|
|
Returns an empty string if we cannot obtain a valid user key; callers
|
|
should skip tests in that case.
|
|
"""
|
|
# Priority 1: Explicit test user key is already a user key - use directly
|
|
explicit_user_key = os.getenv("OPENVIKING_CLI_TEST_API_KEY") or os.getenv(
|
|
"OPENVIKING_USER_API_KEY", ""
|
|
)
|
|
if explicit_user_key:
|
|
return explicit_user_key
|
|
|
|
# Priority 2: If we don't have a root key, we can't provision users; return
|
|
# CONFIGURED_API_KEY as-is (it might be a pre-provisioned user key)
|
|
if not ROOT_API_KEY:
|
|
return CONFIGURED_API_KEY
|
|
|
|
# Priority 3: Use root key to provision/get a user key via admin API.
|
|
# Retry a few times to handle server startup race conditions.
|
|
account_id = CLI_ACCOUNT or "test-account"
|
|
user_id = CLI_USER or "test-user"
|
|
admin_user_id = f"{user_id}-admin"
|
|
|
|
for _attempt in range(5):
|
|
try:
|
|
list_resp = httpx.get(
|
|
f"{BASE_URL}/api/v1/admin/accounts/{account_id}/users",
|
|
headers=_admin_headers(),
|
|
timeout=10.0,
|
|
)
|
|
if list_resp.status_code == 404:
|
|
create_resp = httpx.post(
|
|
f"{BASE_URL}/api/v1/admin/accounts",
|
|
headers=_admin_headers(),
|
|
json={"account_id": account_id, "admin_user_id": admin_user_id},
|
|
timeout=10.0,
|
|
)
|
|
if create_resp.status_code in (200, 201):
|
|
user_key = _register_regular_user(account_id, user_id)
|
|
if user_key:
|
|
return user_key
|
|
elif list_resp.status_code == 200:
|
|
users = list_resp.json().get("result", [])
|
|
if isinstance(users, list):
|
|
candidates = [user_id] if CLI_USER else [user_id, f"{user_id}-regular"]
|
|
for candidate_user_id in candidates:
|
|
user_record = _find_user(users, candidate_user_id)
|
|
if user_record is None:
|
|
user_key = _register_regular_user(account_id, candidate_user_id)
|
|
elif user_record.get("role") == "user":
|
|
api_key = user_record.get("api_key")
|
|
user_key = api_key if isinstance(api_key, str) else ""
|
|
if not user_key:
|
|
key_resp = httpx.post(
|
|
f"{BASE_URL}/api/v1/admin/accounts/{account_id}/users/"
|
|
f"{candidate_user_id}/key",
|
|
headers=_admin_headers(),
|
|
json={},
|
|
timeout=10.0,
|
|
)
|
|
if key_resp.status_code == 200:
|
|
regenerated = key_resp.json().get("result", {}).get("user_key")
|
|
user_key = regenerated if isinstance(regenerated, str) else ""
|
|
else:
|
|
user_key = ""
|
|
if user_key:
|
|
return user_key
|
|
except Exception:
|
|
pass
|
|
time.sleep(2)
|
|
|
|
# Failed to provision a user key after retries - return empty string
|
|
# rather than falling back to root key (which doesn't work for data plane)
|
|
return ""
|
|
|
|
|
|
API_KEY = _resolve_api_key()
|
|
|
|
|
|
def _resolve_bin():
|
|
if OPENVIKING_BIN:
|
|
if os.path.isfile(OPENVIKING_BIN):
|
|
return OPENVIKING_BIN
|
|
import sys as _sys
|
|
|
|
bin_dir = os.path.dirname(_sys.executable)
|
|
for name in ("openviking", "ov"):
|
|
for d in [bin_dir, "/usr/local/bin", "/usr/bin", os.path.expanduser("~/.local/bin")]:
|
|
candidate = os.path.join(d, name)
|
|
if os.path.isfile(candidate):
|
|
return candidate
|
|
for name in ("openviking", "ov"):
|
|
found = shutil.which(name)
|
|
if found:
|
|
return found
|
|
try:
|
|
result = subprocess.run(
|
|
["bash", "-lc", "command -v openviking || command -v ov"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
path = result.stdout.strip()
|
|
if path and os.path.isfile(path):
|
|
return path
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
CLI_BIN = _resolve_bin()
|
|
|
|
|
|
def _write_cli_config():
|
|
config_dir = os.path.join(tempfile.gettempdir(), "openviking_cli_test_config")
|
|
os.makedirs(config_dir, exist_ok=True)
|
|
config_path = os.path.join(config_dir, "ovcli.conf")
|
|
config = {
|
|
"url": BASE_URL,
|
|
"api_key": API_KEY,
|
|
"timeout": 120.0,
|
|
"output": "table",
|
|
"echo_command": True,
|
|
"upload": {},
|
|
}
|
|
if CLI_ACCOUNT:
|
|
config["account"] = CLI_ACCOUNT
|
|
if CLI_USER:
|
|
config["user"] = CLI_USER
|
|
with open(config_path, "w", encoding="utf-8") as f:
|
|
json.dump(config, f, indent=2)
|
|
return config_path
|
|
|
|
|
|
CLI_CONFIG_PATH = _write_cli_config()
|
|
ADD_RESOURCE_WAIT_TIMEOUT = "300"
|
|
ADD_RESOURCE_COMMAND_TIMEOUT = 360
|
|
WRITE_WAIT_TIMEOUT = "300"
|
|
WRITE_COMMAND_TIMEOUT = 360
|
|
|
|
|
|
def _env():
|
|
env = os.environ.copy()
|
|
env["OPENVIKING_CLI_CONFIG_FILE"] = CLI_CONFIG_PATH
|
|
env["OPENVIKING_URL"] = BASE_URL
|
|
env["OPENVIKING_API_KEY"] = API_KEY
|
|
return env
|
|
|
|
|
|
def _check_cli_compatible():
|
|
if CLI_BIN is None:
|
|
return False
|
|
try:
|
|
result = subprocess.run(
|
|
[CLI_BIN, "version"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
env=_env(),
|
|
)
|
|
if result.returncode != 0 and "GLIBC" in result.stderr:
|
|
return False
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
CLI_COMPATIBLE = _check_cli_compatible()
|
|
|
|
|
|
def _apply_cli_skip_markers(items):
|
|
skip_reason = None
|
|
if not CLI_COMPATIBLE:
|
|
skip_reason = "openviking CLI not available"
|
|
if CLI_BIN is None:
|
|
skip_reason = "openviking CLI binary not found. Install via: curl -fsSL http://openviking.tos-cn-beijing.volces.com/cli/install.sh | bash"
|
|
else:
|
|
try:
|
|
result = subprocess.run(
|
|
[CLI_BIN, "version"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
env=_env(),
|
|
)
|
|
if "GLIBC" in result.stderr:
|
|
skip_reason = "openviking CLI binary is not compatible with this system (GLIBC version mismatch)"
|
|
except Exception:
|
|
pass
|
|
elif not API_KEY:
|
|
skip_reason = "Could not obtain a valid user API key for data-plane operations"
|
|
|
|
skip_cli = pytest.mark.skip(reason=skip_reason) if skip_reason else None
|
|
for item in items:
|
|
if item.get_closest_marker("cli_remote"):
|
|
if "ensure_resources_dir" not in item.fixturenames:
|
|
item.fixturenames.append("ensure_resources_dir")
|
|
if skip_cli is not None:
|
|
item.add_marker(skip_cli)
|
|
|
|
|
|
def _parse_cli_json(stdout):
|
|
json_start = stdout.find("{")
|
|
if json_start == -1:
|
|
return None
|
|
json_str = stdout[json_start:]
|
|
try:
|
|
return json.loads(json_str)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
depth = 0
|
|
for i, ch in enumerate(json_str):
|
|
if ch == "{":
|
|
depth += 1
|
|
elif ch == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
try:
|
|
return json.loads(json_str[: i + 1])
|
|
except json.JSONDecodeError:
|
|
break
|
|
return None
|
|
|
|
|
|
def is_cli_auth_error(result: dict) -> bool:
|
|
"""Check if a CLI result indicates an authentication error.
|
|
|
|
Matches both raw API errors (UNAUTHENTICATED/Unauthorized) and the
|
|
user-friendly Rust CLI error card ("Authentication Error" / "rejected the API key").
|
|
"""
|
|
stderr = (result.get("stderr") or "").lower()
|
|
stdout = (result.get("stdout") or "").lower()
|
|
combined = stderr + " " + stdout
|
|
auth_markers = [
|
|
"unauthenticated",
|
|
"authentication error",
|
|
"rejected the api key",
|
|
"unauthorized",
|
|
"forbidden",
|
|
"invalid api key",
|
|
"api key invalid",
|
|
]
|
|
return any(marker in combined for marker in auth_markers)
|
|
|
|
|
|
def skip_if_auth_error(result: dict) -> None:
|
|
"""Skip the test if the result indicates an authentication error."""
|
|
if is_cli_auth_error(result):
|
|
pytest.skip("Upstream API authentication unavailable")
|
|
|
|
|
|
def _inject_global_args(args):
|
|
global_args = []
|
|
if CLI_ACCOUNT:
|
|
global_args.extend(["--account", CLI_ACCOUNT])
|
|
if CLI_USER:
|
|
global_args.extend(["--user", CLI_USER])
|
|
if not args or args[0] in ("version", "--version", "-V", "help", "--help", "-h"):
|
|
return args
|
|
return global_args + args
|
|
|
|
|
|
def ov(args, timeout=120):
|
|
cmd = [CLI_BIN] + _inject_global_args(args)
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
env=_env(),
|
|
)
|
|
stdout = result.stdout.strip()
|
|
data = _parse_cli_json(stdout)
|
|
return {
|
|
"exit_code": result.returncode,
|
|
"json": data,
|
|
"stdout": stdout,
|
|
"stderr": result.stderr.strip(),
|
|
}
|
|
|
|
|
|
def _is_retryable_api_error(result: dict) -> bool:
|
|
"""Check if a CLI error is retryable (resource busy, conflict, network issue)."""
|
|
if is_cli_auth_error(result):
|
|
return False
|
|
stderr = (result.get("stderr") or "").lower()
|
|
stdout = (result.get("stdout") or "").lower()
|
|
combined = stderr + " " + stdout
|
|
retryable_markers = [
|
|
"conflict",
|
|
"resource is busy",
|
|
"busy",
|
|
"network error",
|
|
"connection error",
|
|
"could not reach openviking",
|
|
"timeout",
|
|
"temporarily unavailable",
|
|
]
|
|
return any(marker in combined for marker in retryable_markers)
|
|
|
|
|
|
def _retry_cli_call(args, *, attempts=15, interval=10, timeout=120):
|
|
"""Generic CLI call retry helper for write operations.
|
|
|
|
Retries on CONFLICT/busy/network errors, skips on auth errors,
|
|
returns immediately on non-retryable errors.
|
|
"""
|
|
r = None
|
|
for _attempt in range(attempts):
|
|
r = ov(args, timeout=timeout)
|
|
if r["exit_code"] == 0:
|
|
return r
|
|
skip_if_auth_error(r)
|
|
if _is_retryable_api_error(r):
|
|
time.sleep(interval)
|
|
continue
|
|
# Non-retryable error, return immediately
|
|
return r
|
|
return r
|
|
|
|
|
|
def ov_add_resource(path, to_uri, *extra_args, attempts=15, interval=10):
|
|
"""Add a resource with retries for CONFLICT/busy/network errors."""
|
|
args = [
|
|
"add-resource",
|
|
path,
|
|
"--to",
|
|
to_uri,
|
|
*extra_args,
|
|
"--wait",
|
|
"--timeout",
|
|
ADD_RESOURCE_WAIT_TIMEOUT,
|
|
"-o",
|
|
"json",
|
|
]
|
|
return _retry_cli_call(
|
|
args, attempts=attempts, interval=interval, timeout=ADD_RESOURCE_COMMAND_TIMEOUT
|
|
)
|
|
|
|
|
|
def ov_add_skill(path, *extra_args, attempts=15, interval=10):
|
|
"""Add a skill with retries for CONFLICT/busy/network errors."""
|
|
args = [
|
|
"add-skill",
|
|
path,
|
|
*extra_args,
|
|
"--wait",
|
|
"-o",
|
|
"json",
|
|
]
|
|
return _retry_cli_call(args, attempts=attempts, interval=interval, timeout=120)
|
|
|
|
|
|
def ov_mkdir(uri, *extra_args, attempts=15, interval=5):
|
|
"""Create a directory with retries for CONFLICT/busy/network errors."""
|
|
args = ["mkdir", uri, *extra_args, "-o", "json"]
|
|
return _retry_cli_call(args, attempts=attempts, interval=interval, timeout=120)
|
|
|
|
|
|
def ov_retry(args, *, attempts=5, interval=5, timeout=120, retry_if=None):
|
|
r = None
|
|
for attempt in range(attempts):
|
|
r = ov(args, timeout=timeout)
|
|
if r["exit_code"] == 0:
|
|
return r
|
|
skip_if_auth_error(r)
|
|
# Determine if we should retry
|
|
should_retry = True
|
|
if retry_if is not None:
|
|
should_retry = retry_if(r)
|
|
else:
|
|
should_retry = _is_retryable_api_error(r)
|
|
if not should_retry:
|
|
return r
|
|
if attempt < attempts - 1:
|
|
time.sleep(interval)
|
|
return r
|
|
|
|
|
|
def ov_rm(uri, *, recursive=True, attempts=15, interval=5):
|
|
args = ["rm", uri, "-o", "json"]
|
|
if recursive:
|
|
args.insert(2, "-r")
|
|
return ov_retry(args, attempts=attempts, interval=interval)
|
|
|
|
|
|
def ov_mv(src_uri, dst_uri):
|
|
return ov_retry(["mv", src_uri, dst_uri, "-o", "json"], attempts=20, interval=15)
|
|
|
|
|
|
def ov_cp(src_uri, dst_uri, *, recursive=False, output="json"):
|
|
args = ["cp"]
|
|
if recursive:
|
|
args.append("-r")
|
|
args.extend([src_uri, dst_uri])
|
|
if output:
|
|
args.extend(["-o", output])
|
|
return ov_retry(args, attempts=20, interval=15)
|
|
|
|
|
|
def ov_write(uri, content, *extra_args):
|
|
return ov_retry(
|
|
[
|
|
"write",
|
|
uri,
|
|
"--content",
|
|
content,
|
|
*extra_args,
|
|
"--wait",
|
|
"--timeout",
|
|
WRITE_WAIT_TIMEOUT,
|
|
"-o",
|
|
"json",
|
|
],
|
|
attempts=15,
|
|
interval=20,
|
|
timeout=WRITE_COMMAND_TIMEOUT,
|
|
)
|
|
|
|
|
|
def ov_reindex(uri):
|
|
return ov_retry(["reindex", uri, "--wait", "true", "-o", "json"], attempts=15, interval=15)
|
|
|
|
|
|
def ov_session_new():
|
|
return ov_retry(["session", "new", "-o", "json"], attempts=5, interval=5)
|
|
|
|
|
|
def ov_session_delete(session_id):
|
|
return ov(["session", "delete", session_id, "-o", "json"])
|
|
|
|
|
|
def _wait_for_resource_ready(uri, retries=20, interval=10):
|
|
for _attempt in range(retries):
|
|
r = ov(["read", uri, "-o", "json"], timeout=30)
|
|
if r["exit_code"] == 0 and len(r["stdout"]) > 0:
|
|
return True
|
|
time.sleep(interval)
|
|
return False
|
|
|
|
|
|
def _find_file_in_pack(pack_uri, retries=10, interval=5):
|
|
for _attempt in range(retries):
|
|
ls_r = ov(["ls", pack_uri, "-o", "json"])
|
|
if ls_r["json"] and "result" in ls_r["json"]:
|
|
items = ls_r["json"]["result"]
|
|
for item in items:
|
|
if isinstance(item, dict) and item.get("isDir") is False:
|
|
return item["uri"]
|
|
time.sleep(interval)
|
|
return None
|
|
|
|
|
|
def pytest_collection_modifyitems(items):
|
|
_apply_cli_skip_markers(items)
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def ensure_resources_dir():
|
|
r = ov_mkdir("viking://resources")
|
|
if r["exit_code"] != 0:
|
|
if "already exists" in (r.get("stderr") or "").lower():
|
|
return
|
|
skip_if_auth_error(r)
|
|
r2 = ov(["stat", "viking://resources", "-o", "json"])
|
|
if r2["exit_code"] != 0:
|
|
skip_if_auth_error(r2)
|
|
pytest.fail(f"Failed to create resources dir: {r['stderr'][:300]}")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def test_dir_uri(ensure_resources_dir):
|
|
uri = f"viking://resources/cli_test_{uuid.uuid4().hex[:8]}"
|
|
r = ov_mkdir(uri)
|
|
assert r["exit_code"] == 0, f"mkdir failed after retries: {r['stderr'][:300]}"
|
|
yield uri
|
|
ov_rm(uri, attempts=10, interval=5)
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def test_pack_uri(test_dir_uri):
|
|
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode="w") as f:
|
|
f.write("# CLI Test\n\nThis is a test file for CLI automation.")
|
|
temp_path = f.name
|
|
try:
|
|
pack_uri = f"{test_dir_uri}/test_pack"
|
|
r = ov_add_resource(temp_path, pack_uri)
|
|
assert r["exit_code"] == 0, f"add-resource failed after retries: {r['stderr']}"
|
|
finally:
|
|
os.unlink(temp_path)
|
|
file_uri = _find_file_in_pack(pack_uri, retries=15, interval=5)
|
|
if file_uri:
|
|
ready = _wait_for_resource_ready(file_uri, retries=20, interval=10)
|
|
assert ready, f"Resource {file_uri} did not become ready in time"
|
|
return pack_uri
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def test_file_uri(test_pack_uri):
|
|
file_uri = _find_file_in_pack(test_pack_uri, retries=10, interval=5)
|
|
assert file_uri is not None, (
|
|
f"Could not find file inside pack {test_pack_uri}. "
|
|
f"ls result: {ov(['ls', test_pack_uri, '-o', 'json'])['stdout'][:300]}"
|
|
)
|
|
return file_uri
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def test_session_id():
|
|
r = ov_session_new()
|
|
assert r["exit_code"] == 0, f"session new failed: {r['stderr']}"
|
|
session_id = r["json"]["result"]["session_id"]
|
|
yield session_id
|
|
ov_session_delete(session_id)
|