Files
codex-app-server/cloud/tests/test_handler_split_conformance.py

173 lines
7.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for the v2 handler mixin split (integration base 02835f6).
Pins the split contract so a future violation fails CI:
- the 94 moved methods still resolve on CloudControlPlaneHandler and
come from their mixin module (behaviour unchanged, only the defining
module moved);
- mixin/support modules never import cloud.server (no cycle), never
call sqlite3.connect and never import psycopg (narrow AST scan, same
as #238 arch_conformance.scan_imports);
- server.py re-exports the moved shared helpers as identical objects,
so `from cloud.server import _task_to_dict` keeps working;
- handler method accounting: 244 total, 150 left in server.py itself,
94 in mixins, no name defined twice. (v5 rebase: main #275 added
5 resident methods that legitimately stay in server.py, so the
stack's 238/144 becomes 243/149; moved count stays 94.
Reconcile: goal live-update adds _update_goal_task in server.py,
so 243/149 becomes 244/150; moved count still 94. The source
line's 239/223/16 counts its 12-mixin split and do not apply on
this line, which keeps the target's 6-mixin split.)
Only the standard library; no real model, no network, no secrets.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(
os.path.dirname(os.path.abspath(__file__)))))
from cloud.arch_conformance import scan_imports # noqa: E402
from cloud.handler_support import ( # noqa: E402
_apply_default_quota, _default_quota_usd_micros, _task_to_dict)
from cloud.server import CloudControlPlaneHandler # noqa: E402
from cloud import server as _server_module # noqa: E402
MOVED = {
"cloud.handler_oauth": [
"_oauth_provider", "_oauth_login", "_oauth_clients",
"_oauth_finish", "_oauth_callback", "_oauth_error_page",
"_oauth_success_page", "_oauth_bind_success_page",
"_oauth_callback_get", "_oauth_bind_route",
"_oauth_bind_initiate", "_oauth_bindings_get",
"_oauth_bindings_delete", "_github_app_public_base",
"_github_app_error_page", "_github_app_setup_state",
"_github_app_setup_page", "_github_app_callback",
"_github_app_status"],
"cloud.handler_identity": [
"_memory_create", "_memory_list", "_memory_supersede",
"_identity_store", "_identity_close", "_identity_profile_get",
"_identity_profile_upsert", "_identity_user_memories_list",
"_identity_user_memory_create", "_identity_user_memory_supersede",
"_identity_user_memory_delete", "_identity_context",
"_auto_memory_store", "_auto_memory_close",
"_auto_memory_settings_get", "_auto_memory_settings_upsert",
"_auto_memory_log", "_auto_memory_run", "_task_auto_memory_flags",
"_project_identity_memory_create",
"_project_identity_memories_list",
"_project_identity_memory_supersede",
"_project_identity_memory_delete",
"_kb_index", "_kb_search", "_project_cost", "_task_cost",
"_relay_models", "_models_overview"],
"cloud.handler_project": [
"_fetch_environment", "_create_environment", "_list_environments",
"_get_environment", "_archive_environment", "_create_project",
"_update_project", "_github_repos_list", "_list_projects",
"_can_manage_members", "_set_member_row", "_project_members_list",
"_resolve_invite_target", "_project_member_invite",
"_project_member_patch", "_project_member_delete",
"_remove_member_row", "_project_exists", "_check_project",
"_project_push_subscription_delete"],
"cloud.handler_automation": [
"_push_create_endpoint", "_push_dispatch_endpoint",
"_automation_create", "_automation_list",
"_todos_list", "_todos_create", "_todos_get", "_todos_update",
"_todos_delete", "_automation_cancel", "_automation_get",
"_automation_bind_get", "_automation_bind", "_automation_unbind",
"_automation_heartbeat", "_automation_update", "_triage_inbox"],
"cloud.handler_fullauto": [
"_full_auto_status", "_full_auto_config_plan",
"_full_auto_validate_action", "_full_auto_merge_gate",
"_full_auto_ci_fix", "_full_auto_release_plan",
"_full_auto_rollback_plan", "_full_auto_audit_list",
"_full_auto_execute"],
}
NEW_MODULES = ["cloud/handler_support.py", "cloud/handler_oauth.py",
"cloud/handler_identity.py", "cloud/handler_project.py",
"cloud/handler_automation.py", "cloud/handler_fullauto.py"]
def _own_methods(cls):
return {name for name, value in vars(cls).items()
if callable(value)}
class TestMovedMethodsResolve(unittest.TestCase):
def test_all_moved_methods_resolve_from_handler(self):
for module, names in MOVED.items():
for name in names:
func = getattr(CloudControlPlaneHandler, name, None)
self.assertIsNotNone(
func, "%s missing on handler" % name)
self.assertEqual(
func.__module__, module,
"%s should be defined in %s" % (name, module))
def test_moved_count_is_94(self):
total = sum(len(names) for names in MOVED.values())
self.assertEqual(total, 94)
class TestNoCyclesOrDrivers(unittest.TestCase):
def test_new_modules_pass_layer_scan(self):
repo_root = os.path.dirname(os.path.dirname(
os.path.dirname(os.path.abspath(__file__))))
for rel in NEW_MODULES:
with open(os.path.join(repo_root, rel),
encoding="utf-8") as f:
scanned = scan_imports(f.read())
self.assertNotIn("server", scanned["cloud"],
"%s must not import cloud.server" % rel)
self.assertFalse(scanned["driver_connect"],
"%s must not call sqlite3.connect" % rel)
self.assertFalse(scanned["driver_import"],
"%s must not import psycopg" % rel)
class TestReexports(unittest.TestCase):
def test_shared_helpers_are_identical_objects(self):
self.assertIs(_server_module._task_to_dict, _task_to_dict)
self.assertIs(_server_module._apply_default_quota,
_apply_default_quota)
self.assertIs(_server_module._default_quota_usd_micros,
_default_quota_usd_micros)
def test_base_plumbing_stays_in_server(self):
own = _own_methods(CloudControlPlaneHandler)
for name in ["_send_json", "_send_html", "_read_body",
"_read_json", "_db", "_authenticate", "do_GET",
"do_POST", "_dispatch", "_dispatch_project",
"_dispatch_task_route"]:
self.assertIn(name, own, "%s must stay in server.py" % name)
class TestMethodAccounting(unittest.TestCase):
# 演练注:合入后总数 244 -> 247、server.py 自有 150 -> 153,
# 增量全是 main 侧方法(_routing_shadow、_resolve_create_
# agent_definition、_validate_create_audit_loop),源线方法
# 零丢失、mixin 侧零增减;此为源线 34b9a34 同款 re-pin。
def test_total_247_no_duplicates(self):
seen = {}
dups = []
classes = [CloudControlPlaneHandler]
for base in CloudControlPlaneHandler.__mro__:
if base.__module__.startswith("cloud.handler_"):
classes.append(base)
total = 0
for cls in classes:
for name in _own_methods(cls):
total += 1
if name in seen:
dups.append((seen[name], cls.__name__, name))
seen[name] = cls.__name__
self.assertEqual(dups, [])
self.assertEqual(total, 247)
self.assertEqual(len(_own_methods(CloudControlPlaneHandler)),
247 - 94)
if __name__ == "__main__":
unittest.main()