- restore _audit_row in cloud/automations.py as a thin wrapper over cloud.auth.audit_on_conn (legacy row + ledger entry pair, user_id system, best-effort self-commit preserved). The stack's gap-4 option A deleted the direct writer; main #275 resident.py imports _audit_row, so the bare rebase broke server import. No direct audit_log INSERT is reintroduced, so the #250 no-bypass scan stays green. - conformance accounting 238/144 -> 243/149: main #275 added 5 resident methods that legitimately stay in server.py; moved count stays 94, no duplicates. - directional: resident x3, audit chain, conformance, automations x3 suites, 51 tests OK
165 lines
7.2 KiB
Python
165 lines
7.2 KiB
Python
"""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: 243 total, 149 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.)
|
|
|
|
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):
|
|
def test_total_243_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, 243)
|
|
self.assertEqual(len(_own_methods(CloudControlPlaneHandler)),
|
|
243 - 94)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|