mirror of https://github.com/sysown/proxysql
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.
289 lines
14 KiB
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();
|
|
}
|