← Files Codex AdvisorARCHIVED FILE
tests/test_advisor_cli.py
27 KB · Nov 7, 2026 · 13:31 UTC
"""Black-box tests for the local Advisor settings entrypoint."""
from __future__ import annotations
import json
import os
import shutil
import signal
import subprocess
import sys
import tempfile
import time
import unittest
import uuid
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CLI = ROOT / "scripts" / "advisor-config.sh"
PARENT = "56565656-5656-7565-8565-565656565656"
FAKE_CODEX = r'''#!/usr/bin/env python3
import json, os, signal, sys, time
from pathlib import Path
if sys.argv[1:] == ["--version"]:
print(os.environ.get("FAKE_CODEX_VERSION", "codex-cli 0.153.2"))
raise SystemExit(0)
if sys.argv[1:] == ["app-server", "--help"]:
print("--ignore-user-config --ignore-rules")
raise SystemExit(0)
if len(sys.argv) > 1 and sys.argv[1] == "app-server":
raise SystemExit(90)
args = sys.argv[1:]
model = args[args.index("--model") + 1]
effort = args[args.index("-c") + 1].split('"')[1]
output = Path(args[args.index("--output-last-message") + 1])
log = Path(os.environ["FAKE_CODEX_LOG"])
rows = json.loads(log.read_text()) if log.exists() else []
rows.append({"model": model, "effort": effort})
log.write_text(json.dumps(rows))
attempt_file = Path(os.environ.get("FAKE_ATTEMPT_FILE", str(log) + ".attempt"))
attempt = int(attempt_file.read_text()) + 1 if attempt_file.exists() else 1
attempt_file.write_text(str(attempt))
case = os.environ.get("FAKE_CODEX_CASE", "valid")
if case == "hang":
ready = Path(os.environ["FAKE_READY_MARKER"])
terminated = Path(os.environ["FAKE_TERM_MARKER"])
def stop(*_args):
terminated.write_text("terminated")
raise SystemExit(143)
signal.signal(signal.SIGTERM, stop)
ready.write_text(str(os.getpid()))
while True:
time.sleep(1)
child = f"{attempt:08x}-1111-7111-8111-{attempt:012x}"
sessions = Path(os.environ["CODEX_HOME"]) / "sessions" / "fixture"
sessions.mkdir(parents=True, exist_ok=True)
events = [
{"type": "session_meta", "payload": {"id": child, "source": "exec", "originator": "codex_exec"}},
{"type": "turn_context", "payload": {"model": model, "effort": effort, "sandbox_policy": {"type": "read-only"}, "permission_profile": {"type": "managed"}}},
]
(sessions / f"rollout-fake-{child}.jsonl").write_text("".join(json.dumps(row) + "\n" for row in events))
print(json.dumps({"type": "thread.started", "thread_id": child}))
response = {
"recommendation": "neutral path", "why": "reason", "strongest_objection": "objection",
"change_my_mind": "evidence", "acceptance_checks": ["check"], "risks": "none",
"follow_up_areas": "none",
}
if case == "invalid-once" and attempt == 1:
response.pop("risks")
output.write_text(json.dumps(response))
'''
class AdvisorCliTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.root = Path(self.temporary.name)
self.home = self.root / "codex"
self.home.mkdir(mode=0o700)
self.binary = self.root / "bin"
self.binary.mkdir()
self.fake = self.binary / "codex"
self.fake.write_text(FAKE_CODEX, encoding="utf-8")
self.fake.chmod(0o700)
self.log = self.root / "codex.log"
self.attempts = self.root / "attempts"
self.env = {
**os.environ,
"CODEX_HOME": str(self.home),
"CODEX_THREAD_ID": PARENT,
"PATH": f"{self.binary}:{os.environ['PATH']}",
"FAKE_CODEX_LOG": str(self.log),
"FAKE_ATTEMPT_FILE": str(self.attempts),
}
self.write_parent_fixture("read-only")
def tearDown(self) -> None:
self.temporary.cleanup()
def write_parent_fixture(self, sandbox: str) -> None:
sessions = self.home / "sessions" / "fixture"
sessions.mkdir(parents=True, exist_ok=True)
rollout = sessions / f"rollout-fake-{PARENT}.jsonl"
rows = [
{"type": "session_meta", "payload": {"id": PARENT, "agent_role": "root"}},
{"type": "turn_context", "payload": {"sandbox_policy": {"type": sandbox}, "permission_profile": {"type": "managed"}}},
]
rollout.write_text("".join(json.dumps(row) + "\n" for row in rows))
def run_cli(
self, *args: str, env: dict[str, str] | None = None, timeout: float = 10
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[str(CLI), *args], text=True, capture_output=True,
env=env or self.env, check=False, timeout=timeout,
)
def invocations(self) -> list[dict[str, str]]:
return json.loads(self.log.read_text()) if self.log.exists() else []
def add(self, model: str = "manual/model") -> None:
result = self.run_cli("--json", "models", "add", model)
self.assertEqual(result.returncode, 0, result.stderr)
def test_cli_output_contract_for_both_rendering_modes(self) -> None:
user_home = self.root / "user-home"
user_home.mkdir()
env = {**self.env, "HOME": str(user_home)}
show_keys = "account_availability catalog_present deadline_seconds legacy_settings live_config message selection_revision selections status usage_journal_enabled"
error_keys = "error message status"
settings_keys = "message settings status"
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
journal_record = {
"schema_version": 1, "consultation_id": str(uuid.uuid4()),
"started_at": now, "finished_at": now,
"tier": "standard", "model": "manual/model", "effort": "high",
"outcome": "failed", "transport_contract_version": "1.4",
"total_duration_ms": 1,
"attempts": [{"number": 1, "duration_ms": 1, "outcome": "runtime_failed",
"usage": {"input": None, "cached_input": None, "output": None, "reasoning": None}}],
"totals": {"input": None, "cached_input": None, "output": None, "reasoning": None},
}
rows = [
(("show",), 0, show_keys, "Advisor live configuration and local non-selection settings"),
(("status",), 0, show_keys, "Advisor live configuration and local non-selection settings"),
(("models", "list"), 0, "catalog message models shipped status", "Advisor shipped, manual, and discovered model evidence"),
(("models", "add", "manual/model"), 0, "catalog message status", "Manual candidate recorded; compatibility is unverified"),
(("models", "add", "bad model"), 2, error_keys, "unsafe model selector"),
(("resolve", "--tier", "standard"), 0, "launch message status", "Immutable Advisor launch record"),
(("deadline",), 0, "deadline_seconds message selection_revision status", "Advisor total deadline"),
(("deadline", "120"), 0, "deadline_seconds message selection_revision status", "Advisor total deadline saved"),
(("set", "--tier", "standard", "--model", "manual/model", "--effort", "high"), 2, error_keys, "set is legacy-only; edit the installed advisor.toml for --tier consultations"),
(("reset",), 2, error_keys, "reset is legacy-only; edit the installed advisor.toml for --tier consultations"),
(("restore",), 0, settings_keys, "Prior legacy settings restored; --tier continues to use advisor.toml"),
(("preset", "trial", "--model", "manual/model", "--effort", "high"), 2, error_keys, "preset model and effort are not currently compatible"),
(("journal", "status"), 0, "enabled message records status", "Content-free usage journal status"),
(("journal", "enable"), 0, settings_keys, "Usage journal enabled"),
(("journal", "status"), 0, "enabled message records status", "Content-free usage journal status"),
(("_journal-record", json.dumps(journal_record)), 0, "message status written", "Usage journal record processed"),
(("journal", "disable"), 0, settings_keys, "Usage journal disabled"),
(("journal", "status"), 0, "enabled message records status", "Content-free usage journal status"),
(("journal", "clear"), 0, "cleared message status", "Usage journal cleared"),
(("_journal-record", "{}"), 2, error_keys, "invalid usage journal record keys"),
(("_consume-canary", "bad"), 2, error_keys, "invalid canary authorization"),
(("_consume-canary",), 0, "launch message status", "Authorized synthetic canary"),
(("doctor",), 0, "account_availability catalog current_compatibility dependencies discovery_protocol errors last_content_free_failure launch_eligibility live_config message parent_runtime prior_settings_backup settings stale_selections status", "Local read-only Advisor diagnostics"),
(("unknown-command",), 2, "", ""),
]
journal_enabled = False
for args, exit_code, keys, message in rows:
for as_json in (True, False):
with self.subTest(args=args, as_json=as_json):
call_args = args
if args[0] == "_consume-canary" and len(args) == 1:
token = ("a" if as_json else "b") * 32
canaries = self.home / "advisor" / "canaries"
canaries.mkdir(mode=0o700, exist_ok=True)
authorization = canaries / f"{token}.json"
authorization.write_text(json.dumps({
"model": "manual/model", "effort": "high",
"codex_version": "codex-cli 0.153.2",
"transport_contract_version": "1.4",
"expires_at": int(time.time()) + 300,
}))
authorization.chmod(0o600)
call_args = (*args, token)
result = self.run_cli(*(('--json',) if as_json else ()), *call_args, env=env)
self.assertEqual(result.returncode, exit_code, result.stderr)
if args[0] == "unknown-command":
self.assertEqual(result.stdout, "")
self.assertTrue(result.stderr.startswith("usage: advisor-config.sh"))
self.assertIn("invalid choice: 'unknown-command'", result.stderr)
continue
payload = json.loads(result.stdout)
self.assertEqual(sorted(payload), sorted(keys.split()))
self.assertEqual(payload["status"], "ok" if exit_code == 0 else "unavailable")
self.assertEqual(payload["message"], message)
if exit_code == 2:
self.assertEqual(payload["error"], message)
else:
self.assertNotIn("error", payload)
compact = as_json or (exit_code == 0 and args[0] in {
"resolve", "_consume-canary", "_journal-record"
})
expected = json.dumps(payload, sort_keys=True,
separators=(",", ":") if compact else None,
indent=None if compact else 2) + "\n"
self.assertEqual(result.stdout, expected)
if args == ("journal", "enable"):
journal_enabled = True
elif args == ("journal", "disable"):
journal_enabled = False
if args[0] == "journal" and args[1] == "status":
self.assertIs(payload["enabled"], journal_enabled)
if args[0] == "_journal-record" and exit_code == 0:
self.assertIs(payload["written"], True)
if args[0] == "resolve":
launch = payload["launch"]
self.assertEqual(sorted(launch), sorted("config_path deadline_seconds effort model selection_revision selection_source source_revision tier transport_contract_version".split()))
self.assertEqual((launch["model"], launch["effort"], launch["deadline_seconds"], launch["selection_source"]),
("gpt-6.1-terra", "high", 300, "live-config"))
self.assertEqual(launch["selection_revision"], launch["source_revision"])
if args[0] == "_consume-canary" and exit_code == 0:
launch = payload["launch"]
self.assertEqual(sorted(launch), sorted("codex_version deadline_seconds effort model selection_revision selection_source tier transport_contract_version".split()))
self.assertEqual((launch["model"], launch["effort"], launch["selection_source"]),
("manual/model", "high", "authorized-synthetic-canary"))
def test_help_json_cold_start_and_doctor_inspect_actual_parent(self) -> None:
help_result = self.run_cli("--help")
self.assertEqual(help_result.returncode, 0)
for command in ("show", "models", "set", "preset", "doctor", "reset", "restore", "deadline", "journal"):
self.assertIn(command, help_result.stdout)
show = self.run_cli("--json", "show")
self.assertEqual(show.returncode, 0, show.stderr)
self.assertEqual(len(show.stdout.splitlines()), 1)
current = json.loads(show.stdout)
self.assertEqual(current["selections"]["standard"]["model"], "gpt-6.1-terra")
self.assertTrue(current["selection_revision"].startswith("sha256:"))
self.assertTrue(current["live_config"]["path"].endswith("advisor.toml"))
doctor = self.run_cli("--json", "doctor")
self.assertEqual(doctor.returncode, 0, doctor.stderr)
payload = json.loads(doctor.stdout)
self.assertEqual(payload["parent_runtime"]["status"], "available")
self.assertEqual(payload["parent_runtime"]["sandbox_policy_type"], "read-only")
self.assertIn("inspecting parent runtime evidence", doctor.stderr)
self.assertFalse((self.home / "advisor").exists())
self.assertEqual(self.invocations(), [])
def test_pristine_default_resolve_works_on_newer_cli_without_setup(self) -> None:
for version in ("codex-cli 0.153.4", "codex-cli 9.0.0"):
result = self.run_cli(
"--json", "resolve", "--tier", "standard",
env={**self.env, "FAKE_CODEX_VERSION": version},
)
self.assertEqual(result.returncode, 0, result.stderr)
launch = json.loads(result.stdout)["launch"]
self.assertEqual(
(launch["model"], launch["effort"]), ("gpt-6.1-terra", "high")
)
self.assertFalse((self.home / "advisor").exists())
self.assertEqual(self.invocations(), [])
def test_doctor_reports_missing_malformed_and_unsafe_evidence_without_inference(self) -> None:
rollout = self.home / "sessions" / "fixture" / f"rollout-fake-{PARENT}.jsonl"
rollout.write_text("not-json\n")
malformed = json.loads(self.run_cli("--json", "doctor").stdout)
self.assertEqual(malformed["parent_runtime"]["status"], "unavailable")
self.write_parent_fixture("danger-full-access")
unsafe = json.loads(self.run_cli("--json", "doctor").stdout)
self.assertEqual(unsafe["parent_runtime"]["status"], "unavailable")
rollout.unlink()
missing = json.loads(self.run_cli("--json", "doctor").stdout)
self.assertEqual(missing["parent_runtime"]["status"], "unavailable")
self.assertEqual(self.invocations(), [])
def test_add_test_select_preset_and_pair_freshness(self) -> None:
self.add()
unauthorized = self.run_cli(
"--json", "models", "test", "manual/model", "--effort", "high",
"--parent-thread", PARENT,
)
self.assertEqual(unauthorized.returncode, 2)
self.assertEqual(self.invocations(), [])
tested = self.run_cli(
"--json", "models", "test", "manual/model", "--effort", "high",
"--authorize-usage", "--parent-thread", PARENT,
"--sessions-dir", str(self.home / "sessions"),
)
self.assertEqual(tested.returncode, 0, tested.stderr)
self.assertFalse(json.loads(tested.stdout)["compatibility"]["selected"])
self.assertEqual(len(self.invocations()), 1)
listed = json.loads(self.run_cli("--json", "models", "list").stdout)
receipts = listed["models"]["candidates"][0]["tested_compatibility"]
self.assertEqual([(row["effort"], row["current"]) for row in receipts], [("high", True)])
stale_env = {**self.env, "FAKE_CODEX_VERSION": "codex-cli 0.154.0"}
retained = json.loads(self.run_cli("--json", "models", "list", env=stale_env).stdout)
self.assertTrue(retained["models"]["candidates"][0]["tested_compatibility"][0]["current"])
selected = self.run_cli(
"--json", "set", "--tier", "standard", "--model", "manual/model", "--effort", "high"
)
self.assertEqual(selected.returncode, 2)
self.assertIn("advisor.toml", selected.stdout)
preset = self.run_cli(
"--json", "preset", "manual", "--model", "manual/model", "--effort", "high"
)
self.assertEqual(preset.returncode, 0, preset.stderr)
status = json.loads(self.run_cli("--json", "status").stdout)
self.assertEqual(status["selections"]["standard"]["model"], "gpt-6.1-terra")
def test_canary_uses_at_most_two_attempts_and_records_only_success(self) -> None:
self.add()
result = self.run_cli(
"--json", "models", "test", "manual/model", "--effort", "high",
"--authorize-usage", "--parent-thread", PARENT,
"--sessions-dir", str(self.home / "sessions"),
env={**self.env, "FAKE_CODEX_CASE": "invalid-once"},
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(len(self.invocations()), 2)
self.assertIn("one fresh corrective retry", result.stderr)
catalog = json.loads((self.home / "advisor" / "catalog.json").read_text())
self.assertEqual(len(catalog["compatibility"]), 1)
def test_invalid_settings_recovery_reset_deadline_journal_and_errors(self) -> None:
self.assertEqual(self.run_cli("--json", "deadline", "120").returncode, 0)
self.assertEqual(self.run_cli("--json", "journal", "enable").returncode, 0)
self.add("kept/model")
settings = self.home / "advisor" / "settings.json"
prior = self.home / "advisor" / "settings.previous.json"
malformed = b'{"schema_version":1,"nested":' + b"[" * 1000
settings.write_bytes(malformed)
invalid = self.run_cli("--json", "show")
self.assertEqual(invalid.returncode, 0)
self.assertEqual(json.loads(invalid.stdout)["legacy_settings"], "invalid")
restored = self.run_cli("--json", "restore")
self.assertEqual(restored.returncode, 0, restored.stderr)
invalid_copy = self.home / "advisor" / "settings.invalid.json"
self.assertEqual(invalid_copy.read_bytes(), malformed)
self.assertTrue(prior.exists())
self.assertEqual(self.run_cli("--json", "deadline", "120").returncode, 0)
self.assertEqual(self.run_cli("--json", "journal", "enable").returncode, 0)
reset = self.run_cli("--json", "reset")
self.assertEqual(reset.returncode, 2)
self.assertIn("advisor.toml", reset.stdout)
catalog = json.loads((self.home / "advisor" / "catalog.json").read_text())
self.assertEqual(catalog["candidates"][0]["model"], "kept/model")
self.assertEqual(self.run_cli("--json", "journal", "disable").returncode, 0)
self.assertEqual(self.run_cli("--json", "journal", "clear").returncode, 0)
def test_doctor_reports_dependencies_stale_selection_and_last_failure(self) -> None:
self.add()
settings_dir = self.home / "advisor"
settings = {
"schema_version": 1, "revision": 1,
"selections": {"standard": {"model": "manual/model", "effort": "high"}, "specialist": {"model": "gpt-6.1-sol", "effort": "high"}},
"presets": {}, "deadline_seconds": 300, "usage_journal_enabled": True,
}
(settings_dir / "settings.json").write_text(json.dumps(settings) + "\n")
os.chmod(settings_dir / "settings.json", 0o600)
record = {
"schema_version": 1, "consultation_id": str(uuid.uuid4()),
"started_at": "2026-09-04T12:00:00Z", "finished_at": "2026-09-04T12:00:01Z",
"tier": "standard", "model": "manual/model", "effort": "high",
"outcome": "failed", "transport_contract_version": "1.4", "total_duration_ms": 1,
"attempts": [{"number": 1, "duration_ms": 1, "outcome": "runtime_failed", "usage": {"input": None, "cached_input": None, "output": None, "reasoning": None}}],
"totals": {"input": None, "cached_input": None, "output": None, "reasoning": None},
}
written = self.run_cli("--json", "_journal-record", json.dumps(record))
self.assertEqual(written.returncode, 0, written.stderr)
doctor = json.loads(self.run_cli("--json", "doctor").stdout)
self.assertEqual(doctor["live_config"]["pairs"]["standard"]["model"], "gpt-6.1-terra")
self.assertTrue(doctor["live_config"]["source_revision"].startswith("sha256:"))
self.assertIsNone(doctor["stale_selections"])
self.assertEqual(doctor["last_content_free_failure"]["outcome"], "failed")
self.assertEqual(doctor["dependencies"]["codex"]["status"], "available")
minimal = self.root / "minimal-bin"
minimal.mkdir()
(minimal / "python3").symlink_to(Path(sys.executable))
(minimal / "dirname").symlink_to("/usr/bin/dirname")
no_codex = {**self.env, "PATH": str(minimal)}
missing = json.loads(self.run_cli("--json", "doctor", env=no_codex).stdout)
self.assertEqual(missing["dependencies"]["codex"]["status"], "missing")
self.assertEqual(missing["dependencies"]["jq"], "missing")
no_python_bin = self.root / "no-python-bin"
no_python_bin.mkdir()
(no_python_bin / "dirname").symlink_to("/usr/bin/dirname")
no_python = self.run_cli(
"--json", "doctor", env={**self.env, "PATH": str(no_python_bin)}
)
self.assertEqual(no_python.returncode, 2)
self.assertIn("python3 is unavailable", no_python.stderr)
def test_real_cli_natural_deadline_reaps_child_and_preserves_unrelated(self) -> None:
self.add()
self.assertEqual(self.run_cli("--json", "deadline", "30").returncode, 0)
ready = self.root / "deadline-ready"
terminated = self.root / "deadline-terminated"
unrelated = subprocess.Popen(["sleep", "45"])
started = time.monotonic()
try:
result = self.run_cli(
"--json", "models", "test", "manual/model", "--effort", "high",
"--authorize-usage", "--parent-thread", PARENT,
"--sessions-dir", str(self.home / "sessions"),
env={**self.env, "FAKE_CODEX_CASE": "hang", "FAKE_READY_MARKER": str(ready), "FAKE_TERM_MARKER": str(terminated)},
timeout=38,
)
elapsed = time.monotonic() - started
self.assertEqual(result.returncode, 2)
self.assertGreaterEqual(elapsed, 28)
self.assertLess(elapsed, 37)
self.assertTrue(ready.exists())
self.assertTrue(terminated.exists())
self.assertIsNone(unrelated.poll())
transport_root = self.home / ".tmp" / "advisor-transport"
self.assertFalse(any(transport_root.glob("run.*")))
self.assertFalse(any((self.home / "advisor" / "canaries").glob("*.json")))
finally:
unrelated.terminate()
unrelated.wait(timeout=3)
def test_real_cli_caller_cancellation_reaps_child_and_preserves_unrelated(self) -> None:
self.add()
ready = self.root / "cancel-ready"
terminated = self.root / "cancel-terminated"
unrelated = subprocess.Popen(["sleep", "20"])
process = subprocess.Popen(
[str(CLI), "--json", "models", "test", "manual/model", "--effort", "high", "--authorize-usage", "--parent-thread", PARENT, "--sessions-dir", str(self.home / "sessions")],
text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env={**self.env, "FAKE_CODEX_CASE": "hang", "FAKE_READY_MARKER": str(ready), "FAKE_TERM_MARKER": str(terminated)},
)
try:
started = time.monotonic()
while not ready.exists() and time.monotonic() - started < 5:
time.sleep(0.05)
self.assertTrue(ready.exists())
process.send_signal(signal.SIGTERM)
process.wait(timeout=8)
self.assertTrue(terminated.exists())
self.assertIsNone(unrelated.poll())
transport_root = self.home / ".tmp" / "advisor-transport"
self.assertFalse(any(transport_root.glob("run.*")))
self.assertFalse(any((self.home / "advisor" / "canaries").glob("*.json")))
finally:
if process.poll() is None:
process.kill()
if process.stdout:
process.stdout.close()
if process.stderr:
process.stderr.close()
unrelated.terminate()
unrelated.wait(timeout=3)
def test_missing_or_symlinked_sibling_fails_without_execution(self) -> None:
helpers = sorted(path.name for path in (ROOT / "scripts").glob("advisor_*.py"))
sandbox_plugin = self.root / "sandbox_plugin"
sandbox_plugin.mkdir()
shutil.copy2(ROOT / "advisor.toml", sandbox_plugin / "advisor.toml")
sandbox_scripts = sandbox_plugin / "scripts"
shutil.copytree(ROOT / "scripts", sandbox_scripts)
wrapper = sandbox_scripts / "advisor-config.sh"
res = subprocess.run(
[str(wrapper), "--json", "status"],
capture_output=True,
text=True,
env=self.env,
check=False,
)
self.assertEqual(res.returncode, 0, res.stderr)
for helper in helpers:
target = sandbox_scripts / helper
backup = self.root / f"{helper}.bak"
shutil.copy2(target, backup)
target.unlink()
res_missing = subprocess.run(
[str(wrapper), "--json", "status"],
capture_output=True,
text=True,
env=self.env,
check=False,
)
self.assertEqual(res_missing.returncode, 2)
self.assertIn("ADVISOR CONFIG:", res_missing.stderr)
self.assertEqual(res_missing.stdout, "")
target.symlink_to(backup)
res_sym = subprocess.run(
[str(wrapper), "--json", "status"],
capture_output=True,
text=True,
env=self.env,
check=False,
)
self.assertEqual(res_sym.returncode, 2)
self.assertIn("ADVISOR CONFIG:", res_sym.stderr)
self.assertEqual(res_sym.stdout, "")
target.unlink()
shutil.copy2(backup, target)
backup.unlink()
if __name__ == "__main__":
unittest.main()
SHA-256: 0edfa5b16f1d35ebb714b177ab4b6c12ad3f4647ab965e290e1c19415058540b