You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
proxysql/test/tap/tests/unit/genai_plugin_load_unit-t.cpp

289 lines
14 KiB

// Step 1 acceptance test for the genai plugin scaffold.
//
// Drives the actual genai .so through load → init → start → stop → unload,
// the same way proxysql will at startup. Step 4.F extended the plugin
// to actually read mcp-* admin variables and the mcp_auth/target profile
// tables during start(), so this test now spins up real in-memory
// SQLite3DBs (instead of the previous fake `char*` stubs) and creates
// the tables genai_start expects.
#include "ProxySQL_PluginManager.h"
#include "sqlite3db.h"
#include "tap.h"
#include <string>
#ifndef PROXYSQL_GENAI_PLUGIN_PATH
#error "PROXYSQL_GENAI_PLUGIN_PATH must be defined"
#endif
namespace {
// Real in-memory SQLite databases. Pre-populated with the table
// shapes genai_start() reads. No data — that's enough to make the
// plugin's admin-DB queries succeed and return empty result sets,
// which is the "fresh-install" scenario.
SQLite3DB* g_admindb = nullptr;
SQLite3DB* g_configdb = nullptr;
SQLite3DB* g_statsdb = nullptr;
void setup_admindb_schema(SQLite3DB* db) {
// Minimal schema to satisfy mcp_load_variables_from_admindb /
// mcp_load_target_auth_map_from_admindb in the plugin. Column
// shapes mirror the canonical DDLs in
// include/ProxySQL_Admin_Tables_Definitions.h so the plugin's
// SELECTs (which name every column by name) succeed.
db->execute("CREATE TABLE IF NOT EXISTS global_variables ("
" variable_name TEXT PRIMARY KEY, variable_value TEXT)");
db->execute("CREATE TABLE IF NOT EXISTS mcp_auth_profiles ("
" auth_profile_id TEXT PRIMARY KEY, db_username TEXT,"
" db_password TEXT, default_schema TEXT DEFAULT '',"
" use_ssl INTEGER NOT NULL DEFAULT 0,"
" ssl_mode TEXT DEFAULT '',"
" comment TEXT DEFAULT '')");
db->execute("CREATE TABLE IF NOT EXISTS mcp_target_profiles ("
" target_id TEXT PRIMARY KEY, protocol TEXT, hostgroup_id INTEGER,"
" auth_profile_id TEXT, description TEXT DEFAULT '',"
" max_rows INTEGER DEFAULT 200, timeout_ms INTEGER DEFAULT 2000,"
" allow_explain INTEGER DEFAULT 1, allow_discovery INTEGER DEFAULT 1,"
" active INTEGER DEFAULT 1, comment TEXT DEFAULT '')");
db->execute("CREATE TABLE IF NOT EXISTS runtime_mcp_auth_profiles AS"
" SELECT * FROM mcp_auth_profiles WHERE 0");
db->execute("CREATE TABLE IF NOT EXISTS runtime_mcp_target_profiles AS"
" SELECT * FROM mcp_target_profiles WHERE 0");
db->execute("CREATE TABLE IF NOT EXISTS mcp_query_rules ("
" rule_id INTEGER PRIMARY KEY, active INTEGER NOT NULL DEFAULT 0,"
" username TEXT, target_id TEXT, schemaname TEXT, tool_name TEXT,"
" match_pattern TEXT, negate_match_pattern INTEGER NOT NULL DEFAULT 0,"
" re_modifiers TEXT, flagIN INTEGER NOT NULL DEFAULT 0, flagOUT INTEGER,"
" replace_pattern TEXT, timeout_ms INTEGER, error_msg TEXT, OK_msg TEXT,"
" log INTEGER, apply INTEGER NOT NULL DEFAULT 1, comment TEXT)");
db->execute("CREATE TABLE IF NOT EXISTS runtime_mcp_query_rules AS"
" SELECT * FROM mcp_query_rules WHERE 0");
}
} // namespace
SQLite3DB* proxysql_plugin_get_admindb() { return g_admindb; }
SQLite3DB* proxysql_plugin_get_configdb() { return g_configdb; }
SQLite3DB* proxysql_plugin_get_statsdb() { return g_statsdb; }
int main() {
plan(45);
g_admindb = new SQLite3DB();
g_configdb = new SQLite3DB();
g_statsdb = new SQLite3DB();
g_admindb->open((char*)":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
g_configdb->open((char*)":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
g_statsdb->open((char*)":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
setup_admindb_schema(g_admindb);
ProxySQL_PluginManager mgr;
std::string err {};
const bool loaded = mgr.load(PROXYSQL_GENAI_PLUGIN_PATH, err);
ok(loaded, "load genai plugin succeeds");
if (!loaded) {
diag("load error: %s", err.c_str());
BAIL_OUT("genai plugin must load before lifecycle assertions");
}
ok(mgr.size() == 1, "exactly one plugin handle after load");
// Phase B (chassis): every plugin's register_schemas callback runs
// here, BEFORE init_all. As of Step 4.G the genai plugin uses this
// callback to publish its admin/config/stats table set, so it MUST
// fire before the size assertions below or we'll see 0 tables in
// every kind.
ok(mgr.invoke_register_schemas_phase(err),
"invoke_register_schemas_phase succeeds");
if (!err.empty()) diag("register_schemas error: %s", err.c_str());
ok(mgr.init_all(err), "init_all succeeds");
if (!err.empty()) diag("init error: %s", err.c_str());
ok(mgr.start_all(err), "start_all succeeds");
if (!err.empty()) diag("start error: %s", err.c_str());
// Runtime-view dispatch: SELECT against runtime_mcp_<X> should
// trigger the chassis dispatcher to invoke the plugin's
// project_*_to_runtime_view callback before the SELECT runs.
// Seed the editable mcp_<X> tables, drive `LOAD MCP PROFILES TO
// RUNTIME` through the plugin command registry, then assert the
// runtime_<X> tables match. This is the end-to-end coverage that
// plugin_runtime_views_unit-t can't provide because that test uses
// synthetic callbacks.
ok(g_admindb->execute(
"INSERT INTO mcp_auth_profiles"
" (auth_profile_id, db_username, db_password, default_schema,"
" use_ssl, ssl_mode, comment)"
" VALUES('a','u','p','',0,'','')") ,
"seed mcp_auth_profiles");
ok(g_admindb->execute(
"INSERT INTO mcp_target_profiles"
" (target_id, protocol, hostgroup_id, auth_profile_id, description,"
" max_rows, timeout_ms, allow_explain, allow_discovery, active, comment)"
" VALUES('t','mysql',1,'a','',200,2000,1,1,1,'')") ,
"seed mcp_target_profiles");
ProxySQL_PluginCommandContext cmd_ctx { g_admindb, g_configdb, g_statsdb };
ProxySQL_PluginCommandResult cmd_result;
const bool dispatched = mgr.dispatch_admin_command(
cmd_ctx, "LOAD MCP PROFILES TO RUNTIME", cmd_result);
ok(dispatched && cmd_result.error_code == 0,
"LOAD MCP PROFILES TO RUNTIME dispatches via plugin (rc=%d, msg=%s)",
cmd_result.error_code, cmd_result.message.c_str());
// The chassis pre-SELECT hook should have refreshed runtime_<X>
// from the plugin's snapshot. In a real Admin SQL flow that hook
// fires during GenericRefreshStatistics; the unit test invokes it
// explicitly because we don't have a full Admin module wired up.
mgr.refresh_runtime_views_for_query(
"SELECT * FROM runtime_mcp_auth_profiles", g_admindb, nullptr, nullptr);
mgr.refresh_runtime_views_for_query(
"SELECT * FROM runtime_mcp_target_profiles", g_admindb, nullptr, nullptr);
ok(g_admindb->return_one_int(
"SELECT COUNT(*) FROM runtime_mcp_auth_profiles") == 1,
"runtime_mcp_auth_profiles populated by project callback");
ok(g_admindb->return_one_int(
"SELECT COUNT(*) FROM runtime_mcp_target_profiles") == 1,
"runtime_mcp_target_profiles populated by project callback");
// SAVE round-trip: edit main.* directly (operator typo), then
// dispatch SAVE MCP PROFILES TO MEMORY and verify the in-memory
// snapshot was written back over the operator's edit. This
// closes the loop on the install/save/project triplet — without
// it, install + project would be tested but SAVE would not.
ok(g_admindb->execute(
"UPDATE mcp_target_profiles SET hostgroup_id=999 WHERE target_id='t'"),
"operator stomps target hostgroup_id=999 in main");
ok(g_admindb->return_one_int(
"SELECT hostgroup_id FROM mcp_target_profiles WHERE target_id='t'") == 999,
"main reflects operator stomp");
ProxySQL_PluginCommandResult save_result;
const bool save_dispatched = mgr.dispatch_admin_command(
cmd_ctx, "SAVE MCP PROFILES TO MEMORY", save_result);
ok(save_dispatched && save_result.error_code == 0,
"SAVE MCP PROFILES TO MEMORY dispatches via plugin (rc=%d, msg=%s)",
save_result.error_code, save_result.message.c_str());
ok(g_admindb->return_one_int(
"SELECT hostgroup_id FROM mcp_target_profiles WHERE target_id='t'") == 1,
"SAVE restored hostgroup_id=1 from in-memory snapshot");
ok(mgr.stop_all(), "stop_all succeeds");
// Verify the plugin handles are still live (pre-destructor).
ok(mgr.size() == 1, "plugin handle still present after stop_all");
// Step 4.G: the plugin now owns the MCP admin / config table set.
// Counts match plugins/genai/src/plugin_tables.cpp.
// admin: 6 (mcp_query_rules, mcp_auth_profiles, mcp_target_profiles
// + their runtime_* siblings)
// config: 3 (the persisted-only variants — no runtime_*)
ok(mgr.tables(ProxySQL_PluginDBKind::admin_db).size() == 6,
"genai plugin registers 6 admin-db tables (got %zu)",
mgr.tables(ProxySQL_PluginDBKind::admin_db).size());
ok(mgr.tables(ProxySQL_PluginDBKind::config_db).size() == 3,
"genai plugin registers 3 config-db tables (got %zu)",
mgr.tables(ProxySQL_PluginDBKind::config_db).size());
// Step 4.F: the plugin registers MCP admin SQL verbs. Verify
// each registered alias resolves back to the canonical command
// via the plugin manager's alias resolver (which is the same
// path admin SQL dispatch uses).
ok(mgr.resolve_alias_to_canonical("LOAD MCP VARIABLES TO RUNTIME") ==
"LOAD MCP VARIABLES TO RUNTIME",
"canonical: LOAD MCP VARIABLES TO RUNTIME registered");
ok(mgr.resolve_alias_to_canonical("LOAD MCP VARIABLES FROM MEMORY") ==
"LOAD MCP VARIABLES TO RUNTIME",
"alias: LOAD MCP VARIABLES FROM MEMORY -> TO RUNTIME");
ok(mgr.resolve_alias_to_canonical("LOAD MCP VARIABLES FROM MEM") ==
"LOAD MCP VARIABLES TO RUNTIME",
"alias: LOAD MCP VARIABLES FROM MEM -> TO RUNTIME");
ok(mgr.resolve_alias_to_canonical("LOAD MCP VARIABLES FROM DISK") ==
"LOAD MCP VARIABLES FROM DISK",
"canonical: LOAD MCP VARIABLES FROM DISK registered");
ok(mgr.resolve_alias_to_canonical("LOAD MCP VARIABLES TO MEMORY") ==
"LOAD MCP VARIABLES FROM DISK",
"alias: LOAD MCP VARIABLES TO MEMORY -> FROM DISK");
ok(mgr.resolve_alias_to_canonical("LOAD MCP VARIABLES FROM CONFIG") ==
"LOAD MCP VARIABLES FROM CONFIG",
"canonical: LOAD MCP VARIABLES FROM CONFIG registered");
ok(mgr.resolve_alias_to_canonical("LOAD MCP PROFILES TO RUNTIME") ==
"LOAD MCP PROFILES TO RUNTIME",
"canonical: LOAD MCP PROFILES TO RUNTIME registered");
ok(mgr.resolve_alias_to_canonical("LOAD MCP PROFILES FROM MEMORY") ==
"LOAD MCP PROFILES TO RUNTIME",
"alias: LOAD MCP PROFILES FROM MEMORY -> TO RUNTIME");
ok(mgr.resolve_alias_to_canonical("LOAD MCP PROFILES TO RUN") ==
"LOAD MCP PROFILES TO RUNTIME",
"alias: LOAD MCP PROFILES TO RUN -> TO RUNTIME");
ok(mgr.resolve_alias_to_canonical("LOAD MCP PROFILES FROM DISK") ==
"LOAD MCP PROFILES FROM DISK",
"canonical: LOAD MCP PROFILES FROM DISK registered");
ok(mgr.resolve_alias_to_canonical("LOAD MCP PROFILES TO MEMORY") ==
"LOAD MCP PROFILES FROM DISK",
"alias: LOAD MCP PROFILES TO MEMORY -> FROM DISK");
// SAVE direction: pull runtime mcp-* values back into main.
ok(mgr.resolve_alias_to_canonical("SAVE MCP VARIABLES TO MEMORY") ==
"SAVE MCP VARIABLES TO MEMORY",
"canonical: SAVE MCP VARIABLES TO MEMORY registered");
ok(mgr.resolve_alias_to_canonical("SAVE MCP VARIABLES FROM RUNTIME") ==
"SAVE MCP VARIABLES TO MEMORY",
"alias: SAVE MCP VARIABLES FROM RUNTIME -> TO MEMORY");
ok(mgr.resolve_alias_to_canonical("SAVE MCP VARIABLES FROM RUN") ==
"SAVE MCP VARIABLES TO MEMORY",
"alias: SAVE MCP VARIABLES FROM RUN -> TO MEMORY");
ok(mgr.resolve_alias_to_canonical("SAVE MCP VARIABLES TO MEM") ==
"SAVE MCP VARIABLES TO MEMORY",
"alias: SAVE MCP VARIABLES TO MEM -> TO MEMORY");
ok(mgr.resolve_alias_to_canonical("SAVE MCP VARIABLES TO DISK") ==
"SAVE MCP VARIABLES TO DISK",
"canonical: SAVE MCP VARIABLES TO DISK registered");
// MCP QUERY RULES verbs.
ok(mgr.resolve_alias_to_canonical("LOAD MCP QUERY RULES TO RUNTIME") ==
"LOAD MCP QUERY RULES TO RUNTIME",
"canonical: LOAD MCP QUERY RULES TO RUNTIME registered");
ok(mgr.resolve_alias_to_canonical("LOAD MCP QUERY RULES FROM MEMORY") ==
"LOAD MCP QUERY RULES TO RUNTIME",
"alias: LOAD MCP QUERY RULES FROM MEMORY -> TO RUNTIME");
ok(mgr.resolve_alias_to_canonical("LOAD MCP QUERY RULES FROM MEM") ==
"LOAD MCP QUERY RULES TO RUNTIME",
"alias: LOAD MCP QUERY RULES FROM MEM -> TO RUNTIME");
ok(mgr.resolve_alias_to_canonical("SAVE MCP QUERY RULES TO MEMORY") ==
"SAVE MCP QUERY RULES TO MEMORY",
"canonical: SAVE MCP QUERY RULES TO MEMORY registered");
ok(mgr.resolve_alias_to_canonical("SAVE MCP QUERY RULES FROM RUNTIME") ==
"SAVE MCP QUERY RULES TO MEMORY",
"alias: SAVE MCP QUERY RULES FROM RUNTIME -> TO MEMORY");
ok(mgr.resolve_alias_to_canonical("SAVE MCP QUERY RULES FROM RUN") ==
"SAVE MCP QUERY RULES TO MEMORY",
"alias: SAVE MCP QUERY RULES FROM RUN -> TO MEMORY");
ok(mgr.resolve_alias_to_canonical("LOAD MCP QUERY RULES FROM DISK") ==
"LOAD MCP QUERY RULES FROM DISK",
"canonical: LOAD MCP QUERY RULES FROM DISK registered");
ok(mgr.resolve_alias_to_canonical("LOAD MCP QUERY RULES TO MEMORY") ==
"LOAD MCP QUERY RULES FROM DISK",
"alias: LOAD MCP QUERY RULES TO MEMORY -> FROM DISK");
ok(mgr.resolve_alias_to_canonical("SAVE MCP QUERY RULES TO DISK") ==
"SAVE MCP QUERY RULES TO DISK",
"canonical: SAVE MCP QUERY RULES TO DISK registered");
ok(mgr.resolve_alias_to_canonical("LOAD GENAI VARIABLES FROM CONFIG") ==
"LOAD GENAI VARIABLES FROM CONFIG",
"canonical: LOAD GENAI VARIABLES FROM CONFIG registered");
// Sanity: an unrelated verb does NOT resolve via the plugin.
ok(mgr.resolve_alias_to_canonical("SELECT 1").empty(),
"unknown SQL does not resolve via plugin command registry");
delete g_admindb;
delete g_configdb;
delete g_statsdb;
return exit_status();
}