← Files Codex AdvisorARCHIVED FILE
tests/test_advisor_config.py
25.1 KB · Oct 2, 2026 · 00:32 UTC
"""Behavior tests for the local Advisor configuration foundation."""
from __future__ import annotations
import importlib.util
import io
import json
import os
import signal
import stat
import subprocess
import tempfile
import threading
import time
import unittest
from contextlib import redirect_stderr
from pathlib import Path
MODULE = Path(__file__).resolve().parents[1] / "scripts" / "advisor_config.py"
SPEC = importlib.util.spec_from_file_location("advisor_config", MODULE)
assert SPEC and SPEC.loader
config = importlib.util.module_from_spec(SPEC)
import sys
sys.modules[SPEC.name] = config
SPEC.loader.exec_module(config)
class AdvisorConfigTests(unittest.TestCase):
def setUp(self) -> None:
self.tmp = tempfile.TemporaryDirectory()
self.codex_home = Path(self.tmp.name) / "codex"
self.codex_home.mkdir(mode=0o700)
self.paths = config.state_paths(self.codex_home)
def tearDown(self) -> None:
self.tmp.cleanup()
def test_defaults_are_read_only_and_do_not_initialize_state(self) -> None:
self.assertEqual(
config.load_settings(self.paths)["selections"]["standard"],
{"model": "gpt-5.6-terra", "effort": "high"},
)
self.assertFalse(self.paths.root.exists())
def test_invalid_json_duplicate_and_unknown_settings_fail_closed(self) -> None:
self.paths.root.mkdir(mode=0o700)
self.paths.settings.write_text(
'{"schema_version":1,"schema_version":1}', encoding="utf-8"
)
os.chmod(self.paths.settings, 0o600)
with self.assertRaises(config.ConfigError):
config.load_settings(self.paths)
bad = config.baseline_settings()
bad["unexpected"] = True
with self.assertRaises(config.ConfigError):
config.validate_settings(bad)
def test_json_null_and_boolean_schema_version_do_not_become_defaults(self) -> None:
self.paths.root.mkdir(mode=0o700)
self.paths.settings.write_text("null\n", encoding="utf-8")
os.chmod(self.paths.settings, 0o600)
with self.assertRaises(config.ConfigError):
config.load_settings(self.paths)
boolean = config.baseline_settings()
boolean["schema_version"] = True
with self.assertRaises(config.ConfigError):
config.validate_settings(boolean)
def test_symlink_and_permissive_paths_are_rejected(self) -> None:
self.paths.root.mkdir(mode=0o700)
target = Path(self.tmp.name) / "target.json"
target.write_text("{}", encoding="utf-8")
os.chmod(target, 0o600)
self.paths.settings.symlink_to(target)
with self.assertRaises(config.ConfigError):
config.load_settings(self.paths)
self.paths.settings.unlink()
os.chmod(self.paths.root, 0o755)
with self.assertRaises(config.ConfigError):
config.load_catalog(self.paths)
def test_revision_guards_concurrent_writers(self) -> None:
initial = config.baseline_settings()
first = config.save_settings(initial, expected_revision=0, paths=self.paths)
self.assertEqual(first["revision"], 1)
outcomes: list[object] = []
def writer() -> None:
try:
outcomes.append(
config.save_settings(first, expected_revision=1, paths=self.paths)[
"revision"
]
)
except Exception as exc: # expected for one writer
outcomes.append(exc)
threads = [threading.Thread(target=writer) for _ in range(2)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
self.assertEqual(sum(item == 2 for item in outcomes), 1)
self.assertEqual(
sum(isinstance(item, config.RevisionConflict) for item in outcomes), 1
)
self.assertEqual(stat.S_IMODE(self.paths.root.stat().st_mode), 0o700)
self.assertEqual(stat.S_IMODE(self.paths.settings.stat().st_mode), 0o600)
def test_future_selector_is_a_candidate_not_a_tier_or_compatibility(self) -> None:
future = "future/provider-model_2049:beta"
catalog = config.add_manual_candidate(future, paths=self.paths)
item = catalog["candidates"][0]
self.assertEqual(item["model"], future)
self.assertEqual(item["provenance"], "manual")
self.assertEqual(catalog["compatibility"], [])
def test_compatibility_binds_pair_and_transport_not_cli_provenance(self) -> None:
version = "codex-cli 0.153.2"
receipt = config.validate_compatibility(
{
"model": "future/x",
"effort": "high",
"codex_version": version,
"transport_contract_version": "1.4",
"verified_at": "1",
}
)
self.assertTrue(
config.compatibility_is_current(
receipt, model="future/x", effort="high"
)
)
self.assertFalse(
config.compatibility_is_current(
receipt, model="future/x", effort="medium"
)
)
self.assertTrue(
config.shipped_default_launch_eligible(
tier="standard", model="gpt-5.6-terra", effort="high"
)
)
self.assertFalse(
config.shipped_default_launch_eligible(
tier="standard", model="gpt-6-astra", effort="high"
)
)
def test_pristine_defaults_resolve_without_cli_probe_or_state(self) -> None:
original = config.current_codex_version
config.current_codex_version = lambda: (_ for _ in ()).throw(AssertionError())
try:
standard = config.resolve_selection("standard", paths=self.paths)
specialist = config.resolve_selection("specialist", paths=self.paths)
finally:
config.current_codex_version = original
self.assertEqual(
(standard["model"], standard["effort"]), ("gpt-5.6-terra", "high")
)
self.assertEqual(
(specialist["model"], specialist["effort"]), ("gpt-6-sol", "high")
)
self.assertFalse(self.paths.root.exists())
def test_shipped_catalog_rejects_malformed_defaults_and_baselines(self) -> None:
shipped = config.load_shipped_models()
malformed = json.loads(json.dumps(shipped))
malformed["defaults"]["standard"]["model"] = "gpt-6-astra"
with self.assertRaises(config.ConfigError):
config.validate_shipped_models(malformed)
malformed = json.loads(json.dumps(shipped))
malformed["models"].append(dict(malformed["models"][0]))
with self.assertRaises(config.ConfigError):
config.validate_shipped_models(malformed)
malformed = json.loads(json.dumps(shipped))
malformed["models"][0]["compatibility_baseline"]["efforts"] = [["high"]]
with self.assertRaises(config.ConfigError):
config.validate_shipped_models(malformed)
def test_explicit_routes_fail_closed_when_transport_evidence_stales(self) -> None:
pair = {"model": "future/model", "effort": "high"}
config.add_manual_candidate(pair["model"], paths=self.paths)
config.record_compatibility(
pair, codex_version="codex-cli 0.153.2", paths=self.paths
)
config.set_selection("standard", pair, paths=self.paths)
config.save_preset("future", pair, paths=self.paths)
catalog = config.load_catalog(self.paths)
catalog["compatibility"][0]["transport_contract_version"] = "1.3"
self.paths.catalog.write_text(json.dumps(catalog) + "\n", encoding="utf-8")
os.chmod(self.paths.catalog, 0o600)
resolved = config.resolve_selection("standard", paths=self.paths)
self.assertEqual(
resolved["model"], "gpt-5.6-terra",
"normal tiers must ignore stale saved selections",
)
with self.assertRaises(config.ConfigError):
config.resolve_selection("standard", preset="future", paths=self.paths)
def test_live_config_requires_exact_safe_toml_and_accepts_future_selector(self) -> None:
original = config.live_config_path
candidate = Path(self.tmp.name) / "advisor.toml"
config.live_config_path = lambda: candidate
try:
candidate.write_text(
"[standard]\nmodel = \"future/provider-2049\"\neffort = \"max\"\n"
"[specialist]\nmodel = \"gpt-6-astra\"\neffort = \"high\"\n"
)
live = config.load_live_config()
self.assertEqual(live["pairs"]["standard"], {"model": "future/provider-2049", "effort": "max"})
self.assertTrue(live["source_revision"].startswith("sha256:"))
candidate.write_text("[standard]\nmodel = \"a\"\neffort = \"high\"\n")
with self.assertRaisesRegex(config.ConfigError, "advisor.toml"):
config.load_live_config()
candidate.write_text(
"[standard]\nmodel = \"a\"\neffort = \"bad\"\n"
"[specialist]\nmodel = \"b\"\neffort = \"high\"\n"
)
with self.assertRaisesRegex(config.ConfigError, "advisor.toml"):
config.load_live_config()
candidate.unlink()
candidate.symlink_to(Path(self.tmp.name) / "target")
with self.assertRaisesRegex(config.ConfigError, "advisor.toml"):
config.load_live_config()
finally:
config.live_config_path = original
def test_live_config_opens_nofollow_and_rejects_nonregular_files_without_blocking(self) -> None:
original_path = config.live_config_path
original_open = config.os.open
candidate = Path(self.tmp.name) / "advisor.toml"
target = Path(self.tmp.name) / "target.toml"
target.write_text(
'[standard]\nmodel = "gpt-5.6-terra"\neffort = "high"\n'
'[specialist]\nmodel = "gpt-6-sol"\neffort = "high"\n',
encoding="utf-8",
)
candidate.write_bytes(target.read_bytes())
config.live_config_path = lambda: candidate
def replace_before_open(path: Path, flags: int) -> int:
candidate.unlink()
candidate.symlink_to(target)
config.os.open = original_open
return original_open(path, flags)
try:
config.os.open = replace_before_open
with self.assertRaisesRegex(config.ConfigError, "advisor.toml"):
config.load_live_config()
candidate.unlink()
os.mkfifo(candidate)
started = time.monotonic()
with self.assertRaisesRegex(config.ConfigError, "regular file"):
config.load_live_config()
self.assertLess(time.monotonic() - started, 1.0)
finally:
config.os.open = original_open
config.live_config_path = original_path
def test_live_config_caps_a_file_that_grows_after_open(self) -> None:
original_path = config.live_config_path
original_read = config.os.read
candidate = Path(self.tmp.name) / "advisor.toml"
candidate.write_text(
'[standard]\nmodel = "gpt-5.6-terra"\neffort = "high"\n'
'[specialist]\nmodel = "gpt-6-sol"\neffort = "high"\n',
encoding="utf-8",
)
config.live_config_path = lambda: candidate
delivered = 0
def growing_read(descriptor: int, count: int) -> bytes:
nonlocal delivered
self.assertLessEqual(count, config.MAX_LIVE_CONFIG_BYTES + 1 - delivered)
chunk = b"x" * count
delivered += len(chunk)
return chunk
try:
config.os.read = growing_read
with self.assertRaisesRegex(config.ConfigError, "too large"):
config.load_live_config()
self.assertEqual(delivered, config.MAX_LIVE_CONFIG_BYTES + 1)
finally:
config.os.read = original_read
config.live_config_path = original_path
def test_reset_only_restores_selections(self) -> None:
settings = config.baseline_settings()
settings["selections"]["standard"] = {"model": "custom/future", "effort": "max"}
settings["presets"] = {"one": {"model": "custom/future", "effort": "max"}}
settings["deadline_seconds"] = 120
config.save_settings(settings, expected_revision=0, paths=self.paths)
reset = config.reset_selections(paths=self.paths)
self.assertEqual(reset["selections"], config.baseline_settings()["selections"])
self.assertEqual(reset["presets"]["one"]["model"], "custom/future")
self.assertEqual(reset["deadline_seconds"], 120)
def test_settings_mutations_keep_one_explicit_prior_revision(self) -> None:
initial = config.save_settings(
config.baseline_settings(), expected_revision=0, paths=self.paths
)
updated = config.set_deadline(120, paths=self.paths)
self.assertEqual(updated["deadline_seconds"], 120)
prior = config.validate_settings(config.read_json(self.paths.prior_settings))
self.assertEqual(prior["revision"], initial["revision"])
self.assertEqual(prior["deadline_seconds"], 300)
restored = config.restore_prior_settings(paths=self.paths)
self.assertEqual(restored["deadline_seconds"], 300)
self.assertGreater(restored["revision"], updated["revision"])
def test_failed_settings_write_preserves_active_and_distinct_prior_bytes(
self,
) -> None:
initial = config.save_settings(
config.baseline_settings(), expected_revision=0, paths=self.paths
)
config.set_deadline(120, paths=self.paths)
active_bytes = self.paths.settings.read_bytes()
prior_bytes = self.paths.prior_settings.read_bytes()
self.assertNotEqual(active_bytes, prior_bytes)
original = config._atomic_write
def fail_settings(path: Path, value: object) -> None:
if path == self.paths.settings:
raise OSError("simulated write failure")
original(path, value)
config._atomic_write = fail_settings
try:
with self.assertRaises(OSError):
config.set_deadline(120, paths=self.paths)
finally:
config._atomic_write = original
self.assertEqual(self.paths.settings.read_bytes(), active_bytes)
self.assertEqual(self.paths.prior_settings.read_bytes(), prior_bytes)
self.assertFalse(list(self.paths.root.glob(".*.tmp")))
def test_explicit_restore_recovers_invalid_active_and_preserves_its_bytes(
self,
) -> None:
initial = config.save_settings(
config.baseline_settings(), expected_revision=0, paths=self.paths
)
config.set_deadline(120, paths=self.paths)
malformed = b'{"schema_version":1,"nested":' + b"[" * 1000
self.paths.settings.write_bytes(malformed)
os.chmod(self.paths.settings, 0o600)
restored = config.restore_prior_settings(paths=self.paths)
self.assertEqual(restored["deadline_seconds"], initial["deadline_seconds"])
self.assertEqual(restored["revision"], initial["revision"] + 1)
self.assertEqual(self.paths.invalid_settings.read_bytes(), malformed)
self.assertEqual(stat.S_IMODE(self.paths.invalid_settings.stat().st_mode), 0o600)
def test_restore_refuses_invalid_or_unsafe_backup(self) -> None:
config.save_settings(config.baseline_settings(), expected_revision=0, paths=self.paths)
self.paths.settings.write_text("null\n", encoding="utf-8")
self.paths.prior_settings.write_text("null\n", encoding="utf-8")
with self.assertRaises(config.ConfigError):
config.restore_prior_settings(paths=self.paths)
self.paths.prior_settings.unlink()
target = Path(self.tmp.name) / "prior-target"
target.write_text("{}\n", encoding="utf-8")
self.paths.prior_settings.symlink_to(target)
with self.assertRaises(config.ConfigError):
config.restore_prior_settings(paths=self.paths)
def test_restore_refuses_to_downgrade_a_newer_settings_schema(self) -> None:
config.save_settings(config.baseline_settings(), expected_revision=0, paths=self.paths)
prior_bytes = self.paths.prior_settings.read_bytes()
future = {"schema_version": 2, "revision": 9, "future": {"setting": True}}
future_bytes = (json.dumps(future) + "\n").encode()
self.paths.settings.write_bytes(future_bytes)
os.chmod(self.paths.settings, 0o600)
with self.assertRaisesRegex(config.ConfigError, "refuses to downgrade"):
config.restore_prior_settings(paths=self.paths)
self.assertEqual(self.paths.settings.read_bytes(), future_bytes)
self.assertEqual(self.paths.prior_settings.read_bytes(), prior_bytes)
self.assertFalse(self.paths.invalid_settings.exists())
def test_canary_supervisor_cancellation_reaps_owned_group_only(self) -> None:
wrapper = Path(self.tmp.name) / "canary-wrapper.sh"
child_marker = Path(self.tmp.name) / "canary-child"
terminated = Path(self.tmp.name) / "canary-terminated"
wrapper.write_text(
"#!/bin/sh\n"
"trap 'printf terminated > \"$2\"; exit 143' TERM\n"
"sleep 20 &\n"
"printf '%s' \"$!\" > \"$1\"\n"
"wait\n",
encoding="utf-8",
)
wrapper.chmod(0o700)
unrelated = subprocess.Popen(["sleep", "20"])
timer = threading.Timer(1.0, os.kill, args=(os.getpid(), signal.SIGTERM))
try:
timer.start()
with self.assertRaises(config.ConfigError):
config._run_compatibility_wrapper(
[str(wrapper), str(child_marker), str(terminated)],
deadline_seconds=30,
on_status=None,
)
self.assertTrue(child_marker.exists())
self.assertTrue(terminated.exists())
self.assertIsNone(unrelated.poll())
finally:
timer.cancel()
unrelated.terminate()
unrelated.wait(timeout=3)
def test_paginated_discovery_uses_model_not_id_and_keeps_hidden_rows(self) -> None:
model = lambda ident, dispatch, hidden=False: {
"id": ident,
"model": dispatch,
"displayName": ident,
"description": "model",
"hidden": hidden,
"isDefault": False,
"defaultReasoningEffort": "high",
"supportedReasoningEfforts": [
{"reasoningEffort": "high", "description": ""}
],
}
pages = [
{"data": [model("marketing-name", "dispatch/one")], "nextCursor": "next"},
{"data": [model("hidden-name", "dispatch/two", True)], "nextCursor": None},
]
rows = config.normalize_discovery_pages(pages, now=lambda: 42)
self.assertEqual(rows[0]["id"], "marketing-name")
self.assertEqual(rows[0]["model"], "dispatch/one")
self.assertFalse(rows[1]["visible"])
self.assertEqual(rows[1]["model"], "dispatch/two")
def test_discovery_requires_schema_fields_and_ignores_optional_metadata(
self,
) -> None:
model = {
"id": "friendly",
"model": "dispatch/model",
"displayName": "Friendly",
"description": "model",
"hidden": False,
"isDefault": True,
"defaultReasoningEffort": "high",
"supportedReasoningEfforts": [
{"reasoningEffort": "high", "description": "default", "extra": 1}
],
"serviceTiers": [],
}
rows = config.normalize_discovery_pages(
[{"data": [model], "nextCursor": None, "optional": True}]
)
self.assertEqual(rows[0]["model"], "dispatch/model")
del model["description"]
with self.assertRaises(config.DiscoveryError):
config.normalize_discovery_pages([{"data": [model], "nextCursor": None}])
def test_models_lists_shipped_catalog_and_local_candidates(self) -> None:
config.add_manual_candidate("future/model", paths=self.paths)
env = {**os.environ, "CODEX_HOME": str(self.codex_home)}
result = subprocess.run(
[str(MODULE.parent / "advisor-config.sh"), "--json", "models"],
env=env,
text=True,
capture_output=True,
check=False,
)
payload = json.loads(result.stdout)
self.assertEqual(result.returncode, 0)
self.assertIn(
"gpt-6-astra", [item["model"] for item in payload["shipped"]["models"]]
)
self.assertEqual(payload["catalog"]["candidates"][0]["model"], "future/model")
def test_discovery_rejects_loop_and_malformed_pages(self) -> None:
with self.assertRaises(config.DiscoveryError):
config.normalize_discovery_pages(
[
{"data": [], "nextCursor": "again"},
{"data": [], "nextCursor": "again"},
]
)
with self.assertRaises(config.DiscoveryError):
config.normalize_discovery_pages(
[{"data": [{"id": "only-id"}], "nextCursor": None}]
)
def test_refresh_preserves_manual_and_marks_missing_discovery_hidden(self) -> None:
config.add_manual_candidate("manual/future", paths=self.paths)
model = {
"id": "old",
"model": "server/old",
"displayName": "Old",
"description": "model",
"hidden": False,
"isDefault": False,
"defaultReasoningEffort": "high",
"supportedReasoningEfforts": [
{"reasoningEffort": "high", "description": ""}
],
}
config.refresh_catalog(
lambda: [{"data": [model], "nextCursor": None}],
paths=self.paths,
now=lambda: 10,
)
updated = config.refresh_catalog(
lambda: [{"data": [], "nextCursor": None}], paths=self.paths, now=lambda: 20
)
self.assertEqual(updated["candidates"][0]["model"], "manual/future")
old = next(row for row in updated["candidates"] if row["model"] == "server/old")
self.assertFalse(old["visible"])
def test_offline_adapter_is_closed_and_does_not_mutate_catalog(self) -> None:
before = config.load_catalog(self.paths)
with self.assertRaises(config.DiscoveryUnavailable):
config.AppServerDiscovery(
lambda: (_ for _ in ()).throw(config.ProcessUnavailable("offline"))
).pages()
self.assertEqual(config.load_catalog(self.paths), before)
def test_adapter_paginates_and_closes_its_owned_connection(self) -> None:
class Connection:
def __init__(self) -> None:
self.calls, self.closed = [], False
def request(self, method, payload):
self.calls.append((method, payload))
if method == "initialize":
return {"serverInfo": {}}
model = lambda ident: {
"id": ident,
"model": ident,
"displayName": ident,
"description": "model",
"hidden": False,
"isDefault": False,
"defaultReasoningEffort": "high",
"supportedReasoningEfforts": [
{"reasoningEffort": "high", "description": ""}
],
}
return (
{"data": [model("a")], "nextCursor": "c"}
if "cursor" not in payload
else {"data": [model("b")], "nextCursor": None}
)
def notify(self, method, payload):
self.calls.append((method, payload))
def close(self):
self.closed = True
connection = Connection()
pages = config.AppServerDiscovery(lambda: connection).pages()
self.assertEqual(len(pages), 2)
self.assertTrue(connection.closed)
self.assertEqual(connection.calls[0][0], "initialize")
self.assertEqual(connection.calls[0][1]["clientInfo"]["name"], "codex-advisor")
self.assertEqual(connection.calls[1][0], "initialized")
def test_cli_refresh_emits_progress_and_never_invokes_inference(self) -> None:
fake_bin = Path(self.tmp.name) / "bin"
fake_bin.mkdir()
fake = fake_bin / "codex"
fake.write_text("#!/bin/sh\nexit 1\n", encoding="utf-8")
fake.chmod(0o700)
env = {
**os.environ,
"CODEX_HOME": str(self.codex_home),
"PATH": f"{fake_bin}:{os.environ['PATH']}",
}
result = subprocess.run(
[str(MODULE.parent / "advisor-config.sh"), "--json", "models", "refresh"],
env=env,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 2)
self.assertIn("discovery started", result.stderr)
self.assertEqual(json.loads(result.stdout)["status"], "unavailable")
self.assertFalse(
self.paths.root.exists(), "a failed read-only refresh must not create state"
)
if __name__ == "__main__":
unittest.main()
SHA-256: 169b5aa46700a4f09c6ee0dbadf3d02b1b6c91465a356de38804d5ebdc8ed9dc