ouroboros/tests/test_smoke.py
Ouroboros c7cd252f2a Pin the alarm clock, the wake message and retarget the retired-loop harnesses
tests/test_consciousness.py is rewritten for tick(now): every branch
(disabled, live wake, live owner turn, not due, allowance unknown/exhausted,
no owner chat, launch with the envelope and rendered message, refusal,
finish -> chosen interval / pending reason / doubling backoff, notify
debounce, the same-instant admission race, self-trigger exclusion, boot floor,
the one-time inbox archive, graceful stop of a live wake, the snapshot keys).
tests/test_consciousness_wake.py pins the renderer (placeholders, event
truncation, fail-soft readers), the template's hints and the server status
projection.

Retired with the loop: test_consciousness_observations.py,
test_observation_fold.py, test_background_model_wait_browser.py, the
background twins in test_openai_chat_dispatch*.py, test_anthropic_native_custody.py,
test_transport_death_retry.py, the background-role pins in
test_owner_live_delivery.py / test_quiz_answer.py, the BG budget pins and the
consciousness max_tokens row. The context harnesses (test_repo_read_limits,
test_context, test_durable_learning_completeness) now assert the same
governance and gap facts on the wake's Main context through build_llm_messages;
test_background_model_wait.py keeps only the generic model-wait facts. The
docs-sync identifier audit resolves the wake template against the full
registry plus its render placeholders; the people/identity prompt pins follow
the new wording; test_settings_honesty pins that the wake-up bounds apply
without a restart; the persistence scan and the smoke tool inventory absorb
the retired fold segments and the registered set_next_wakeup.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
2026-09-16 06:03:21 +03:00

677 lines
28 KiB
Python

"""Smoke test suite for Ouroboros.
Tests core invariants:
- All modules import cleanly
- Tool registry discovers all expected tools
- Utility functions work correctly
- Memory operations don't crash
- Context builder produces valid structure
- Bible invariants hold (no hardcoded replies, version sync)
Run: python -m pytest tests/test_smoke.py -v
"""
import os
import pathlib
import re
import sys
import tempfile
import pytest
REPO = pathlib.Path(__file__).resolve().parent.parent
# ── Module imports ───────────────────────────────────────────────
CORE_MODULES = [
"ouroboros.agent",
"ouroboros.context",
"ouroboros.loop",
"ouroboros.llm",
"ouroboros.memory",
"ouroboros.review",
"ouroboros.git_shell_policy",
"ouroboros.protected_artifacts",
"ouroboros.shell_parse",
"ouroboros.utils",
"ouroboros.consciousness",
"ouroboros.tool_capabilities",
]
TOOL_MODULES = [
"ouroboros.tools.registry",
"ouroboros.tools.core",
"ouroboros.tools.git",
"ouroboros.tools.shell",
"ouroboros.tools.search",
"ouroboros.tools.control",
"ouroboros.tools.browser",
"ouroboros.tools.review",
"ouroboros.tools.claude_advisory_review",
"ouroboros.tools.recent_tasks",
"ouroboros.tools.scope_review",
"ouroboros.tools.review_helpers",
"ouroboros.tools.plan_review",
"ouroboros.tools.git_rollback",
"ouroboros.tools.git_pr",
"ouroboros.tools.github",
"ouroboros.tools.ci",
"ouroboros.tools.verify",
]
SUPERVISOR_MODULES = [
"supervisor.state",
"supervisor.message_bus",
"supervisor.queue",
"supervisor.workers",
"supervisor.git_ops",
"supervisor.events",
]
@pytest.mark.parametrize("module", CORE_MODULES + TOOL_MODULES + SUPERVISOR_MODULES)
def test_import(module):
"""Every module imports without error."""
__import__(module)
# ── Tool registry ────────────────────────────────────────────────
@pytest.fixture
def registry():
from ouroboros.tools.registry import ToolRegistry
tmp = pathlib.Path(tempfile.mkdtemp())
return ToolRegistry(repo_dir=tmp, drive_root=tmp)
def test_tool_set_matches(registry):
"""Tool registry contains exactly the expected tools (no more, no less)."""
schemas = registry.schemas()
actual_tools = {t["function"]["name"] for t in schemas}
expected_tools = set(EXPECTED_TOOLS)
missing = expected_tools - actual_tools
extra = actual_tools - expected_tools
assert missing == set(), f"Missing tools: {sorted(missing)}"
assert extra == set(), f"Extra tools: {sorted(extra)}"
assert actual_tools == expected_tools, "Tool set mismatch"
EXPECTED_TOOLS = [
"browse_page", "browser_action",
"run_ci_tests",
"preflight_review", "review_status",
"compact_context", "set_tool_timeout", "request_restart",
"promote_to_stable", "schedule_subagent", "schedule_followup",
"configure_presence", "initiate_presence",
"integrate_subagent_patch", "compare_subagent_patches",
# C1: the explicit acceptance seam for a delegated run's captured patch —
# a first-class tool, so the registry contract must name it.
"integrate_delegated_patch", "cancel_task",
"peek_task", "discard_child_result", "override_delegation_constraint",
"request_deep_self_review", "chat_history", "update_scratchpad",
"send_user_message", "update_identity", "toggle_evolution",
"toggle_consciousness",
"set_next_wakeup", "switch_model", "get_task_result",
"wait_task", "wait_tasks", "tree_note", "tree_read",
"delegate_start", "delegate_wait", "delegate_cancel", "delegate_answer",
"read_file", "list_files", "write_file", "edit_text",
"apply_patch", "edit_batch",
"send_photo", "send_video", "send_file", "send_links", "search_code", "query_code", "escalate",
"forward_to_worker",
"generate_evolution_stats",
"commit_reviewed", "vcs_commit_reviewed", "vcs_status", "vcs_diff",
"vcs_pull_ff", "vcs_restore", "vcs_revert",
"fetch_pr_ref", "create_integration_branch", "cherry_pick_pr_commits",
"stage_adaptations", "stage_pr_merge", "vcs_rollback",
"list_github_prs", "get_github_pr", "comment_on_pr",
"list_github_issues", "get_github_issue", "comment_on_issue",
"close_github_issue", "create_github_issue",
"codebase_health", "knowledge_read", "knowledge_write", "knowledge_list",
"journal_read", "journal_write", "workpad_read", "workpad_write",
"promote_chat_to_task", "route_to_project", "list_projects", "steer_task",
"ensure_project_scope", "schedule_followup",
"memory_map", "memory_update_registry",
"plan_task", "recent_tasks", "task_acceptance_review", "verify_and_record", "web_search",
"start_service", "service_status", "service_logs", "stop_service",
"run_command", "run_script",
"list_skills", "skill_review", "skill_exec", "toggle_skill", "skill_owner_action",
"skill_preflight", "submit_skill_to_hub",
"list_available_tools", "enable_tools",
"analyze_screenshot", "vlm_query", "view_image",
"ocr_pdf", "youtube_transcript", "extract_video_frames",
]
@pytest.mark.parametrize("tool_name", EXPECTED_TOOLS)
def test_tool_registered(registry, tool_name):
"""Each expected tool is in the registry."""
available = [t["function"]["name"] for t in registry.schemas()]
assert tool_name in available, f"{tool_name} not in registry"
def test_unknown_tool_returns_warning(registry):
"""Calling unknown tool returns warning, not exception."""
result = registry.execute("__nonexistent__", {})
assert "Unknown tool" in result or "⚠️" in result
def test_tool_schemas_valid(registry):
"""All tool schemas have required OpenAI fields."""
for schema in registry.schemas():
assert schema["type"] == "function"
func = schema["function"]
assert "name" in func
assert "description" in func
assert isinstance(func["description"], str)
assert "parameters" in func
params = func["parameters"]
assert params["type"] == "object"
assert "properties" in params
def test_tool_schemas_have_no_empty_enum_values(registry):
"""No tool-parameter `enum` may contain an empty/blank string.
Google Gemini's function-calling validator rejects empty enum values with
HTTP 400 INVALID_ARGUMENT ("enum[0]: cannot be empty"), which silently forces
a per-round fallback to another provider. OpenAI/Anthropic accept empty enums,
so this only surfaces against live Gemini — hence this cheap static guard over
the whole assembled tool-schema set. Express "no choice" by OMITTING the
optional param, never by an empty enum member."""
def _walk(node, path):
if isinstance(node, dict):
enum = node.get("enum")
if isinstance(enum, list):
bad = [v for v in enum if isinstance(v, str) and v.strip() == ""]
assert not bad, f"empty enum value at {path}: {enum!r}"
for key, value in node.items():
_walk(value, f"{path}.{key}")
elif isinstance(node, list):
for i, item in enumerate(node):
_walk(item, f"{path}[{i}]")
for schema in registry.schemas():
_walk(schema.get("function", {}).get("parameters", {}), schema.get("function", {}).get("name", "?"))
def test_github_create_issue_schema_fields(registry):
schema = registry.get_schema_by_name("create_github_issue")["function"]
props = schema["parameters"]["properties"]
assert schema["parameters"]["required"] == ["title"]
assert props["title"]["type"] == "string"
assert props["body"]["type"] == "string"
assert props["body"]["default"] == ""
assert props["labels"]["type"] == "string"
assert props["labels"]["default"] == ""
def test_tool_execute_basic(registry):
"""Actually execute a simple tool to verify execution works."""
result = registry.execute("run_command", {"cmd": ["echo", "hello"]})
assert isinstance(result, str), "Tool execute should return string"
assert "hello" in result.lower() or "⚠️" in result, "Should return output or error"
def test_frozen_registry_includes_packaged_tool_modules(monkeypatch):
"""Frozen-mode registry must still load packaged tool modules."""
from ouroboros.tools.registry import ToolRegistry
tmp = pathlib.Path(tempfile.mkdtemp())
monkeypatch.setattr(sys, "frozen", True, raising=False)
registry = ToolRegistry(repo_dir=tmp, drive_root=tmp)
available = {t["function"]["name"] for t in registry.schemas()}
expected_subset = {
"memory_map",
"memory_update_registry",
"preflight_review",
"review_status",
"plan_task",
"vcs_rollback",
"run_ci_tests",
# github.py is in _FROZEN_TOOL_MODULES — PR inspection tools must work in frozen builds
"list_github_prs",
"get_github_pr",
"comment_on_pr",
"query_code",
}
missing = expected_subset - available
assert missing == set(), f"Frozen registry missing tools: {sorted(missing)}"
# ── Utilities ────────────────────────────────────────────────────
def test_safe_relpath_normal():
from ouroboros.utils import safe_relpath
result = safe_relpath("foo/bar.py")
assert result == "foo/bar.py"
def test_safe_relpath_rejects_traversal():
from ouroboros.utils import safe_relpath
with pytest.raises(ValueError):
safe_relpath("../../../etc/passwd")
def test_safe_relpath_strips_leading_slash():
"""safe_relpath strips leading / but doesn't raise."""
from ouroboros.utils import safe_relpath
result = safe_relpath("/etc/passwd")
assert not result.startswith("/")
def test_clip_text():
from ouroboros.utils import clip_text
# Test 1: Long text gets clipped (max_chars=500)
long_text = "hello world " * 100 # ~1200 chars
result = clip_text(long_text, 500)
assert len(result) < len(long_text), "Long text should be clipped"
assert len(result) > 0, "Result should not be empty"
assert "...(truncated)..." in result, "Truncation marker should be present"
# Test 2: Short text passes through unchanged
short_text = "hello world"
result_short = clip_text(short_text, 500)
assert result_short == short_text, "Short text should pass through unchanged"
def test_estimate_tokens():
from ouroboros.utils import estimate_tokens
tokens = estimate_tokens("Hello world, this is a test.")
assert 5 <= tokens <= 20
# ── Memory ───────────────────────────────────────────────────────
def test_memory_scratchpad():
"""Memory reads/writes scratchpad without crash."""
from ouroboros.memory import Memory
with tempfile.TemporaryDirectory() as tmp:
from ouroboros.utils import write_text
mem = Memory(drive_root=pathlib.Path(tmp))
write_text(mem.scratchpad_path(), "test content")
content = mem.load_scratchpad()
assert "test content" in content
def test_memory_identity():
"""Memory reads/writes identity without crash."""
from ouroboros.memory import Memory
with tempfile.TemporaryDirectory() as tmp:
mem = Memory(drive_root=pathlib.Path(tmp))
# Write identity file directly (identity_path is a method)
mem.identity_path().parent.mkdir(parents=True, exist_ok=True)
mem.identity_path().write_text("I am Ouroboros", encoding="utf-8")
content = mem.load_identity()
assert "Ouroboros" in content
def test_memory_chat_history_empty():
"""Chat history returns string when no data."""
from ouroboros.memory import Memory
with tempfile.TemporaryDirectory() as tmp:
mem = Memory(drive_root=pathlib.Path(tmp))
history = mem.chat_history(count=10)
assert isinstance(history, str)
def test_memory_persistence():
"""Memory persists across instances (write with one, read with another)."""
from ouroboros.memory import Memory
with tempfile.TemporaryDirectory() as tmp:
tmp_path = pathlib.Path(tmp)
# Write with first instance
from ouroboros.utils import write_text
mem1 = Memory(drive_root=tmp_path)
write_text(mem1.scratchpad_path(), "test persistence content")
# Read with second instance
mem2 = Memory(drive_root=tmp_path)
content = mem2.load_scratchpad()
assert "test persistence content" in content, "Memory should persist across instances"
# ── Context builder ─────────────────────────────────────────────
# ── Bible invariants ─────────────────────────────────────────────
def test_no_hardcoded_replies():
"""Principle 5 (LLM-First): no hardcoded reply strings in code.
Checks for suspicious patterns like:
- reply = "Fixed string"
- return "Sorry, I can't..."
"""
suspicious = re.compile(
r'(reply|response)\s*=\s*["\'](?!$|{|\s*$)',
re.IGNORECASE,
)
violations = []
for root, dirs, files in os.walk(REPO / "ouroboros"):
dirs[:] = [d for d in dirs if d != "__pycache__"]
for f in files:
if not f.endswith(".py"):
continue
path = pathlib.Path(root) / f
for i, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if line.strip().startswith("#"):
continue
if suspicious.search(line):
if "{" in line or "f'" in line or 'f"' in line:
continue
violations.append(f"{path.name}:{i}: {line.strip()}")
assert len(violations) < 5, "Possible hardcoded replies:\n" + "\n".join(violations)
def test_version_file_exists():
"""VERSION file exists and contains a valid PEP 440 version.
Stable releases carry plain ``X.Y.Z``; pre-releases carry
``X.Y.Z[-]?(rc|alpha|beta|a|b)\\.?N`` per the ``release_sync``
carrier-format contract. Both are accepted here; stricter
spelling rules live in ``tests/test_release_sync.py``.
"""
from ouroboros.tools.release_sync import _VERSION_RE
version = (REPO / "VERSION").read_text(encoding="utf-8").strip()
assert _VERSION_RE.match(version), (
f"VERSION '{version}' is not a valid semver / PEP 440 pre-release token"
)
def test_version_in_readme():
"""VERSION matches what README claims."""
version = (REPO / "VERSION").read_text(encoding="utf-8").strip()
readme = (REPO / "README.md").read_text(encoding="utf-8")
assert version in readme, f"VERSION {version} not found in README.md"
def test_bible_exists_and_has_principles():
"""BIBLE.md exists and contains the current principle set (0-13)."""
bible = (REPO / "BIBLE.md").read_text(encoding="utf-8")
principles = re.findall(r"^## Principle (\d+):", bible, flags=re.MULTILINE)
assert principles == [str(i) for i in range(14)], f"Unexpected BIBLE principles: {principles}"
# ── Code quality invariants ──────────────────────────────────────
def test_no_env_dumping():
"""Security: no code dumps entire env (os.environ without key access).
Allows: os.environ["KEY"], os.environ.get(), os.environ.setdefault(),
os.environ.copy() (for subprocess).
Disallows: print(os.environ), json.dumps(os.environ), etc.
"""
# Only flag raw os.environ passed to print/json/log without bracket or .get( accessor
dangerous = re.compile(r'(?:print|json\.dumps|log)\s*\(.*\bos\.environ\b(?!\s*[\[.])')
violations = []
for root, dirs, files in os.walk(REPO):
dirs[:] = [d for d in dirs if d not in ('.git', '__pycache__', 'tests')]
for f in files:
if not f.endswith(".py"):
continue
path = pathlib.Path(root) / f
for i, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if line.strip().startswith("#"):
continue
if dangerous.search(line):
violations.append(f"{path.name}:{i}: {line.strip()[:80]}")
assert len(violations) == 0, "Dangerous env dumping:\n" + "\n".join(violations)
# The `size_ratchet` lane below BLOCKS only in official-repository CI (a
# dedicated `pytest tests/ -m size_ratchet` step in quick-test and full-test);
# default local runs exclude the marker and surface the same validator
# findings as warnings via check_worktree_readiness and codebase_health.
# Canonical lane description: docs/DEVELOPMENT.md "Pytest marker lanes".
@pytest.mark.size_ratchet
def test_no_oversized_modules():
"""Principle 7: exact-path debt is the only exception to the hard gate."""
from ouroboros.review import GIANT_PATHS, MAX_MODULE_LINES, iter_gated_modules
max_lines = MAX_MODULE_LINES
violations = [
f"{module.path}: {module.line_count} lines"
for module in iter_gated_modules(REPO)
if module.line_count > max_lines and module.path not in GIANT_PATHS
]
assert len(violations) == 0, f"Oversized modules (>{max_lines} lines):\n" + "\n".join(violations)
@pytest.mark.size_ratchet
def test_size_ratchet_manifest_matches_live_tree():
"""Exact module/function/band/byte debt matches the untruncated candidate tree."""
from ouroboros.review import validate_size_ratchet
errors = validate_size_ratchet(REPO)
assert not errors, "Size-ratchet manifest violations:\n" + "\n".join(errors)
@pytest.mark.size_ratchet
def test_size_ratchet_transition_against_explicit_base():
"""Official CI enforces the pairwise base-vs-tip shrink-only transition.
CI exports ``OURO_SIZE_RATCHET_BASE_REF`` (PR base SHA / push
``event.before``); without it the check degrades to the tip's parent
manifest — the merge-aware local semantics. An all-zeros base (new-branch /
tag push) degrades the same way (never a skip), while manifest exactness
stays enforced by ``test_size_ratchet_manifest_matches_live_tree``.
"""
from ouroboros.review import validate_size_ratchet_transition_against_base
base_ref = os.environ.get("OURO_SIZE_RATCHET_BASE_REF") or None
errors = validate_size_ratchet_transition_against_base(REPO, base_ref)
assert not errors, (
f"Size-ratchet pairwise transition violations (base={base_ref or 'HEAD-parent'}):\n" + "\n".join(errors)
)
def test_js_module_gate_buckets_and_grandfathering():
"""The JS size gate sees web/tests and exempts debt by exact rel-path only."""
from ouroboros.review import compute_complexity_metrics, module_is_grandfathered
sections = [
("repo/web/app.js", "x\n" * 2000), # gated, over hard gate, not grandfathered
("repo/web/modules/chat.js", "x\n" * 4000), # gated, grandfathered by rel-path
("repo/web/vendor/chart.umd.min.js", "x\n" * 9000), # vendored/minified — excluded
("repo/web/tests/foo.test.js", "x\n" * 9000), # web/tests/ — gated
("repo/ouroboros/small.py", "x\n" * 10),
]
metrics = compute_complexity_metrics(sections)
assert metrics["js_files"] == 3 # app.js + chat.js + web/tests; vendored excluded
oversized = {p for p, _n in metrics["oversized_modules"]}
grandfathered = {p for p, _n in metrics["grandfathered_modules"]}
assert "web/app.js" in oversized
assert "web/modules/chat.js" in grandfathered
assert "web/modules/chat.js" not in oversized
assert "web/tests/foo.test.js" in oversized
assert "web/vendor/chart.umd.min.js" not in oversized
assert "web/vendor/chart.umd.min.js" not in grandfathered
# Grandfather entry is rel-path-keyed: a chat.js anywhere else stays gated.
assert module_is_grandfathered("web/modules/chat.js")
assert not module_is_grandfathered("repo/web/modules/chat.js")
assert not module_is_grandfathered("web/other/chat.js")
def test_no_bare_except_pass():
"""No bare `except: pass` (not even except Exception: pass with just pass).
v4.9.0 hardened exceptions — but checks the STRICTEST form:
bare except (no Exception class) followed by pass.
"""
violations = []
for root, dirs, files in os.walk(REPO / "ouroboros"):
dirs[:] = [d for d in dirs if d != "__pycache__"]
for f in files:
if not f.endswith(".py"):
continue
path = pathlib.Path(root) / f
lines = path.read_text(encoding="utf-8").splitlines()
for i, line in enumerate(lines, 1):
stripped = line.strip()
# Only flag bare `except:` (no class specified)
if stripped == "except:":
# Check next non-empty line is just `pass`
for j in range(i, min(i + 3, len(lines))):
next_line = lines[j].strip()
if next_line and next_line == "pass":
violations.append(f"{path.name}:{i}: bare except: pass")
break
assert len(violations) == 0, "Bare except:pass found:\n" + "\n".join(violations)
# ── AST-based function size check ───────────────────────────────
def _get_function_sizes():
"""Return exact path/qualname tuples from the production iterator."""
from ouroboros.review import iter_gated_functions
return [(item.path, item.qualname, item.line_count) for item in iter_gated_functions(REPO)]
@pytest.mark.size_ratchet
def test_no_extremely_oversized_functions():
"""No function exceeds the hard gate."""
from ouroboros.review import FUNCTION_DEBT, MAX_FUNCTION_LINES
violations = []
for path, qualname, size in _get_function_sizes():
if (path, qualname) in FUNCTION_DEBT:
continue
if size > MAX_FUNCTION_LINES:
violations.append(f"{path}:{qualname} = {size} lines")
assert len(violations) == 0, \
f"Functions exceeding {MAX_FUNCTION_LINES} lines:\n" + "\n".join(violations)
def test_function_count_has_sanity_floor():
"""The production inventory still discovers a plausible codebase."""
sizes = _get_function_sizes()
assert len(sizes) >= 100, f"Only {len(sizes)} functions — too few?"
# ── Pre-push gate tests ──────────────────────────────────────────────
class TestPrePushGate:
"""Tests for pre-push test gate in git.py."""
def test_run_pre_push_tests_disabled(self):
"""When OUROBOROS_PRE_PUSH_TESTS=0, should return None (skip)."""
import os
from ouroboros.tools.git import _run_pre_push_tests
old = os.environ.get("OUROBOROS_PRE_PUSH_TESTS")
try:
os.environ["OUROBOROS_PRE_PUSH_TESTS"] = "0"
# ctx doesn't matter since we return early
result = _run_pre_push_tests(None)
assert result is None
finally:
if old is None:
os.environ.pop("OUROBOROS_PRE_PUSH_TESTS", None)
else:
os.environ["OUROBOROS_PRE_PUSH_TESTS"] = old
def test_run_pre_push_tests_no_tests_dir(self):
"""When tests/ dir doesn't exist, should return None."""
from ouroboros.tools.git import _run_pre_push_tests
import os
old = os.environ.get("OUROBOROS_PRE_PUSH_TESTS")
try:
os.environ["OUROBOROS_PRE_PUSH_TESTS"] = "1"
# Create a mock ctx with non-existent repo_dir
class FakeCtx:
repo_dir = "/tmp/nonexistent_repo_dir_12345"
result = _run_pre_push_tests(FakeCtx())
assert result is None
finally:
if old is None:
os.environ.pop("OUROBOROS_PRE_PUSH_TESTS", None)
else:
os.environ["OUROBOROS_PRE_PUSH_TESTS"] = old
def test_git_commit_with_tests_exists(self):
"""_git_commit_with_tests helper exists and is callable."""
from ouroboros.tools.git import _git_commit_with_tests
assert callable(_git_commit_with_tests)
def test_pre_push_tests_timeout_is_sufficient(self):
"""The pre-push/post-commit pytest budget must be >= 180s.
Since v6.88.0 this is the TOTAL budget across the node test lane AND
both preflight passes (parallel ``not serial``, then ``serial``, which
gets the remainder) — not a per-pass one. A full suite takes MINUTES,
and one loaded-host run measured 1077s, so a cap sized for an idle host
reports a healthy suite as a timeout. The budget is owned by ``run_hermetic_pytest``
(default + ``OUROBOROS_PREFLIGHT_TIMEOUT_SEC`` env) so callers do not
re-pin a stale literal — this guard anchors on that single source of truth.
"""
from ouroboros.preflight_runner import (
_DEFAULT_PREFLIGHT_TIMEOUT_SEC,
_resolve_preflight_timeout,
)
assert _DEFAULT_PREFLIGHT_TIMEOUT_SEC >= 180, (
f"preflight default timeout is {_DEFAULT_PREFLIGHT_TIMEOUT_SEC}s — 180s is a "
"FLOOR against a caller re-pinning a tiny literal, never a claim about how "
"long the suite takes; the real budget must cover a whole loaded-host run."
)
# The env override is honoured so operators can raise it on slow hosts.
import os as _os
prev = _os.environ.get("OUROBOROS_PREFLIGHT_TIMEOUT_SEC")
try:
_os.environ["OUROBOROS_PREFLIGHT_TIMEOUT_SEC"] = "600"
assert _resolve_preflight_timeout(_DEFAULT_PREFLIGHT_TIMEOUT_SEC) == 600
finally:
if prev is None:
_os.environ.pop("OUROBOROS_PREFLIGHT_TIMEOUT_SEC", None)
else:
_os.environ["OUROBOROS_PREFLIGHT_TIMEOUT_SEC"] = prev
def test_no_module_defines_the_same_top_level_name_twice():
"""A redefinition is invisible at import: the later one silently wins.
This is the shape a clean git merge produces when two branches independently add
the same concept — no conflict markers, no import error, no failing test, just one
definition quietly shadowing another. It happened in this very series: two branches
each added `SUBAGENT_EXECUTORS` and `normalize_subagent_executor` to subagents.py,
one a frozenset and one a tuple, and the merge picked a winner by position. That
pair happened to be equivalent; the next one will not be.
"""
import ast
import collections
import pathlib
root = pathlib.Path(__file__).resolve().parents[1]
offenders = []
for path in sorted(root.rglob("*.py")):
parts = set(path.parts)
if parts & {".git", "node_modules", "venv", ".venv", "build", "dist"}:
continue
try:
tree = ast.parse(path.read_text(encoding="utf-8"))
except (SyntaxError, UnicodeDecodeError):
continue
seen = collections.Counter()
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
seen[node.name] += 1
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
seen[node.target.id] += 1
elif isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name):
seen[target.id] += 1
for name, count in seen.items():
# Dunders and TYPE_CHECKING/try-except import shims legitimately rebind.
if count > 1 and not name.startswith("__"):
offenders.append(f"{path.relative_to(root)}: {name} defined {count}x")
assert not offenders, "Top-level names defined more than once:\n" + "\n".join(offenders)