All checks were successful
技能自动化发布 / release (push) Successful in 9s
Co-authored-by: Cursor <cursoragent@cursor.com>
333 lines
10 KiB
Python
333 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""v1 CLI compatibility — subprocess-only, mirrors monitor-competitor-rates calling patterns."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from concurrent.futures import ThreadPoolExecutor, wait
|
|
|
|
import pytest
|
|
|
|
_scripts = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts")
|
|
if _scripts not in sys.path:
|
|
sys.path.insert(0, _scripts)
|
|
|
|
MAIN_PY = os.path.join(_scripts, "main.py")
|
|
|
|
|
|
def _cli_env(roots: dict) -> dict:
|
|
e = {**os.environ}
|
|
e["JIANGCHANG_DATA_ROOT"] = roots["data_root"]
|
|
e["JIANGCHANG_USER_ID"] = roots["user_id"]
|
|
e["CLAW_DATA_ROOT"] = roots["data_root"]
|
|
e["CLAW_USER_ID"] = roots["user_id"]
|
|
return e
|
|
|
|
|
|
def _run_cli(env: dict, args: list[str], stdin: str | None = None) -> subprocess.CompletedProcess:
|
|
inp = stdin.encode("utf-8") if stdin is not None else None
|
|
return subprocess.run(
|
|
[sys.executable, MAIN_PY, *args],
|
|
input=inp,
|
|
capture_output=True,
|
|
env=env,
|
|
cwd=os.path.dirname(_scripts),
|
|
)
|
|
|
|
|
|
def _parse_last_json(stdout: str) -> dict:
|
|
lines = [ln for ln in (stdout or "").strip().split("\n") if ln.strip()]
|
|
assert lines, "stdout had no non-empty lines"
|
|
return json.loads(lines[-1])
|
|
|
|
|
|
def run_cli_json_last(env: dict, args: list[str], *, stdin: str | None = None) -> dict:
|
|
"""Parse **last** stdout line as JSON (monitor `_parse_stdout_json` semantics)."""
|
|
r = _run_cli(env, args, stdin=stdin)
|
|
assert r.returncode == 0, (r.stdout, r.stderr)
|
|
return _parse_last_json(r.stdout.decode("utf-8"))
|
|
|
|
|
|
@pytest.fixture()
|
|
def iso_cli_env():
|
|
tmp = tempfile.mkdtemp(prefix="v1_compat_")
|
|
data_root = os.path.join(tmp, "claw-data")
|
|
os.makedirs(data_root)
|
|
uid = "compat_user"
|
|
yield {"data_root": data_root, "user_id": uid, "tmp_dir": tmp}
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
|
|
|
|
def _add_monitor_account(env: dict) -> dict:
|
|
out = run_cli_json_last(
|
|
env,
|
|
[
|
|
"account",
|
|
"add-web",
|
|
"--platform",
|
|
"maersk",
|
|
"--label",
|
|
"monitor-test",
|
|
"--environment",
|
|
"simulator",
|
|
"--role",
|
|
"booking",
|
|
"--tenant-id",
|
|
"tenant-test",
|
|
],
|
|
)
|
|
assert out.get("success") is True
|
|
return out["data"]
|
|
|
|
|
|
class TestV1PickWeb:
|
|
def test_pick_web_returns_v1_shape(self, iso_cli_env):
|
|
env = _cli_env(iso_cli_env)
|
|
add_data = _add_monitor_account(env)
|
|
aid = add_data["id"]
|
|
assert isinstance(aid, int)
|
|
|
|
pick = run_cli_json_last(
|
|
env,
|
|
[
|
|
"account",
|
|
"pick-web",
|
|
"--platform",
|
|
"maersk",
|
|
"--environment",
|
|
"simulator",
|
|
"--role",
|
|
"booking",
|
|
"--lease",
|
|
"--holder",
|
|
"monitor-competitor-rates",
|
|
"--purpose",
|
|
"maersk_sim_rpa",
|
|
"--ttl-sec",
|
|
"1800",
|
|
],
|
|
)
|
|
|
|
for key in ("id", "platform_key", "environment", "role", "profile_dir", "lease_token"):
|
|
assert key in pick, f"pick-web 缺少 v1 字段 {key}"
|
|
|
|
assert isinstance(pick["id"], int)
|
|
assert isinstance(pick["lease_token"], str) and len(pick["lease_token"]) > 0
|
|
assert pick["platform_key"] == "maersk"
|
|
assert pick["environment"] == "simulator"
|
|
assert pick["role"] == "booking"
|
|
|
|
for v2_key in ("auth_strategy", "session_persistent", "device_fingerprint"):
|
|
assert v2_key in pick, f"v2 字段 {v2_key} 应在 pick-web 返回中"
|
|
|
|
assert len(pick) >= 10
|
|
|
|
def test_pick_web_no_account_returns_legacy_error_code(self, iso_cli_env):
|
|
env = _cli_env(iso_cli_env)
|
|
proc = _run_cli(
|
|
env,
|
|
[
|
|
"account",
|
|
"pick-web",
|
|
"--platform",
|
|
"maersk",
|
|
"--environment",
|
|
"simulator",
|
|
"--role",
|
|
"booking",
|
|
"--lease",
|
|
"--holder",
|
|
"monitor-competitor-rates",
|
|
],
|
|
)
|
|
assert proc.returncode == 0
|
|
payload = _parse_last_json(proc.stdout.decode("utf-8"))
|
|
assert payload.get("success") is False
|
|
assert payload["error"]["code"] == "ERROR:NO_ACCOUNT"
|
|
|
|
def test_pick_web_lease_race_returns_legacy_error_shape(self, iso_cli_env):
|
|
"""Parallel picks share one account — second outcome may be NO_ACCOUNT or LEASE_CONFLICT."""
|
|
env = _cli_env(iso_cli_env)
|
|
_add_monitor_account(env)
|
|
|
|
args = [
|
|
"account",
|
|
"pick-web",
|
|
"--platform",
|
|
"maersk",
|
|
"--environment",
|
|
"simulator",
|
|
"--role",
|
|
"booking",
|
|
"--lease",
|
|
"--holder",
|
|
"monitor-competitor-rates",
|
|
"--ttl-sec",
|
|
"3600",
|
|
]
|
|
|
|
def _pick():
|
|
return _run_cli(env, args)
|
|
|
|
with ThreadPoolExecutor(max_workers=2) as ex:
|
|
f1 = ex.submit(_pick)
|
|
f2 = ex.submit(_pick)
|
|
wait([f1, f2])
|
|
proc_a = f1.result()
|
|
proc_b = f2.result()
|
|
|
|
payloads = []
|
|
for proc in (proc_a, proc_b):
|
|
assert proc.returncode == 0
|
|
payloads.append(_parse_last_json(proc.stdout.decode("utf-8")))
|
|
|
|
codes = []
|
|
successes = 0
|
|
for p in payloads:
|
|
if p.get("success") is False:
|
|
codes.append(p["error"]["code"])
|
|
elif "lease_token" in p:
|
|
successes += 1
|
|
|
|
assert successes >= 1
|
|
assert "ERROR:LEASE_CONFLICT" in codes or "ERROR:NO_ACCOUNT" in codes
|
|
|
|
|
|
class TestV1LeaseAndMarks:
|
|
def test_lease_release_returns_success_json(self, iso_cli_env):
|
|
env = _cli_env(iso_cli_env)
|
|
_add_monitor_account(env)
|
|
pick = run_cli_json_last(
|
|
env,
|
|
[
|
|
"account",
|
|
"pick-web",
|
|
"--platform",
|
|
"maersk",
|
|
"--environment",
|
|
"simulator",
|
|
"--role",
|
|
"booking",
|
|
"--lease",
|
|
"--holder",
|
|
"monitor-competitor-rates",
|
|
"--ttl-sec",
|
|
"900",
|
|
],
|
|
)
|
|
tok = pick["lease_token"]
|
|
rel = run_cli_json_last(env, ["lease", "release", tok])
|
|
assert rel.get("success") is True
|
|
assert rel["data"]["status"] == "released"
|
|
|
|
def test_mark_used_returns_success(self, iso_cli_env):
|
|
env = _cli_env(iso_cli_env)
|
|
aid = _add_monitor_account(env)["id"]
|
|
out = run_cli_json_last(env, ["account", "mark-used", str(aid)])
|
|
assert out.get("success") is True
|
|
assert int(out["data"]["id"]) == aid
|
|
|
|
def test_mark_session_accepts_v1_status_values(self, iso_cli_env):
|
|
env = _cli_env(iso_cli_env)
|
|
aid = _add_monitor_account(env)["id"]
|
|
for status in ("likely_valid", "needs_login", "unknown"):
|
|
out = run_cli_json_last(
|
|
env,
|
|
["account", "mark-session", str(aid), "--session-status", status],
|
|
)
|
|
assert out.get("success") is True
|
|
assert out["data"]["session_status"] == status
|
|
|
|
def test_mark_error_accepts_long_message(self, iso_cli_env):
|
|
env = _cli_env(iso_cli_env)
|
|
aid = _add_monitor_account(env)["id"]
|
|
long_msg = "x" * 600
|
|
out = run_cli_json_last(
|
|
env,
|
|
[
|
|
"account",
|
|
"mark-error",
|
|
str(aid),
|
|
"--code",
|
|
"ERR_TEST",
|
|
"--message",
|
|
long_msg,
|
|
],
|
|
)
|
|
assert out.get("success") is True
|
|
assert out["data"]["error_code"] == "ERR_TEST"
|
|
|
|
|
|
class TestV1EndToEnd:
|
|
def test_full_monitor_sequence_end_to_end(self, iso_cli_env):
|
|
env = _cli_env(iso_cli_env)
|
|
aid = _add_monitor_account(env)["id"]
|
|
|
|
pick1 = run_cli_json_last(
|
|
env,
|
|
[
|
|
"account",
|
|
"pick-web",
|
|
"--platform",
|
|
"maersk",
|
|
"--environment",
|
|
"simulator",
|
|
"--role",
|
|
"booking",
|
|
"--lease",
|
|
"--holder",
|
|
"monitor-competitor-rates",
|
|
"--purpose",
|
|
"maersk_sim_rpa",
|
|
"--ttl-sec",
|
|
"900",
|
|
],
|
|
)
|
|
tok = pick1["lease_token"]
|
|
assert pick1["id"] == aid
|
|
|
|
run_cli_json_last(env, ["account", "mark-used", str(aid)])
|
|
run_cli_json_last(
|
|
env,
|
|
["account", "mark-session", str(aid), "--session-status", "likely_valid"],
|
|
)
|
|
run_cli_json_last(env, ["lease", "release", tok])
|
|
|
|
pick2 = run_cli_json_last(
|
|
env,
|
|
[
|
|
"account",
|
|
"pick-web",
|
|
"--platform",
|
|
"maersk",
|
|
"--environment",
|
|
"simulator",
|
|
"--role",
|
|
"booking",
|
|
"--lease",
|
|
"--holder",
|
|
"monitor-competitor-rates",
|
|
"--ttl-sec",
|
|
"900",
|
|
],
|
|
)
|
|
assert "lease_token" in pick2
|
|
|
|
acc = run_cli_json_last(env, ["account", "get", str(aid)])
|
|
assert acc.get("session_status") == "likely_valid"
|
|
|
|
|
|
class TestStdoutIsolation:
|
|
def test_stderr_does_not_pollute_stdout_last_line(self, iso_cli_env):
|
|
env = _cli_env(iso_cli_env)
|
|
proc = _run_cli(env, ["account", "export-credentials", "--plaintext"])
|
|
assert proc.returncode == 0
|
|
assert b"[export] WARNING" in proc.stderr
|
|
lines = [ln for ln in proc.stdout.decode("utf-8").strip().split("\n") if ln.strip()]
|
|
payload = json.loads(lines[-1])
|
|
assert isinstance(payload, list)
|