From 2538e303cf928d9f0f40f2fa4ee9d570e47cd4f5 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 17 Feb 2026 01:28:02 +0000 Subject: [PATCH] tap ai: add dual-backend static-harvest fixtures and target_id coverage test Implement phase-A (static harvesting) TAP coverage for MCP multi-target discovery by seeding deterministic schemas on both MySQL and PostgreSQL and validating discovery/catalog behavior per target_id. What this commit adds: - AI group deterministic seed datasets - Added test/tap/groups/ai/mysql-seed.sql with: - tap_mysql_static_customers - tap_mysql_static_orders (FK to customers) - Added test/tap/groups/ai/pgsql-seed.sql with: - tap_pgsql_static_accounts - tap_pgsql_static_events (FK to accounts) - pre-proxysql hook integration - Updated test/tap/groups/ai/pre-proxysql.bash to seed both backends after container startup: - seed_mysql_test_data() executes mysql-seed.sql via mysql CLI - seed_pgsql_test_data() executes pgsql-seed.sql via psql (or docker compose exec fallback) - Existing monitor-user/profile setup is preserved - New TAP test: test/tap/tests/test_mcp_static_harvest-t.sh - Validates MCP/ProxySQL reachability - Validates list_targets exposes both mysql and pgsql target_id entries - Runs discovery.run_static for MySQL target_id and validates run_id + protocol=mysql - Validates catalog.list_objects returns seeded MySQL table for that run - Runs discovery.run_static for PostgreSQL target_id and validates run_id + protocol=pgsql - Validates catalog.list_objects returns seeded PostgreSQL table for that run - Validates run isolation across targets (cross-target run_id lookup fails as expected) - Documentation update - Updated test/tap/groups/ai/README.md with seeded-table details and manual run instructions for the new static-harvest test Notes: - This commit focuses strictly on phase-A static harvesting, as requested. - Phase-B (LLM-driven discovery) tests are intentionally not included here. --- test/tap/groups/ai/README.md | 13 ++- test/tap/groups/ai/mysql-seed.sql | 27 +++++ test/tap/groups/ai/pgsql-seed.sql | 24 ++++ test/tap/groups/ai/pre-proxysql.bash | 16 +++ test/tap/tests/test_mcp_static_harvest-t.sh | 118 ++++++++++++++++++++ 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 test/tap/groups/ai/mysql-seed.sql create mode 100644 test/tap/groups/ai/pgsql-seed.sql create mode 100755 test/tap/tests/test_mcp_static_harvest-t.sh diff --git a/test/tap/groups/ai/README.md b/test/tap/groups/ai/README.md index a96fd17fd..1ef3eb3b6 100644 --- a/test/tap/groups/ai/README.md +++ b/test/tap/groups/ai/README.md @@ -13,6 +13,9 @@ Both are started from `test/tap/groups/ai/docker-compose.yml`. - `pre-proxysql.bash` - starts local containers (`docker-compose-init.bash`) + - seeds deterministic static-harvest datasets on both backends: + - MySQL: `tap_mysql_static_customers`, `tap_mysql_static_orders` + - PostgreSQL: `tap_pgsql_static_accounts`, `tap_pgsql_static_events` - enables MCP - configures backend hostgroups and MCP profiles/targets: - MySQL target: `tap_mysql_default` @@ -41,8 +44,16 @@ bash test/tap/tests/test_mcp_query_rules-t.sh bash test/tap/groups/ai/post-proxysql.bash ``` +For static-harvest phase-A suite (mysql + pgsql targets): + +```bash +source test/tap/groups/ai/env.sh +bash test/tap/groups/ai/pre-proxysql.bash +bash test/tap/tests/test_mcp_static_harvest-t.sh +bash test/tap/groups/ai/post-proxysql.bash +``` + ## Notes - All variables can be overridden from the environment before running hooks. - The scripts still work in Jenkins-driven TAP flows because they do not require Jenkins-only paths. - diff --git a/test/tap/groups/ai/mysql-seed.sql b/test/tap/groups/ai/mysql-seed.sql new file mode 100644 index 000000000..190753821 --- /dev/null +++ b/test/tap/groups/ai/mysql-seed.sql @@ -0,0 +1,27 @@ +CREATE DATABASE IF NOT EXISTS testdb; +USE testdb; + +CREATE TABLE IF NOT EXISTS tap_mysql_static_customers ( + customer_id INT PRIMARY KEY, + email VARCHAR(128) NOT NULL UNIQUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS tap_mysql_static_orders ( + order_id INT PRIMARY KEY, + customer_id INT NOT NULL, + total_amount DECIMAL(10,2) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_tap_mysql_orders_customer + FOREIGN KEY (customer_id) REFERENCES tap_mysql_static_customers(customer_id) +); + +INSERT INTO tap_mysql_static_customers(customer_id, email) VALUES + (1, 'seed-mysql-a@example.com'), + (2, 'seed-mysql-b@example.com') +ON DUPLICATE KEY UPDATE email=VALUES(email); + +INSERT INTO tap_mysql_static_orders(order_id, customer_id, total_amount) VALUES + (101, 1, 42.50), + (102, 2, 18.99) +ON DUPLICATE KEY UPDATE total_amount=VALUES(total_amount); diff --git a/test/tap/groups/ai/pgsql-seed.sql b/test/tap/groups/ai/pgsql-seed.sql new file mode 100644 index 000000000..7dad3d115 --- /dev/null +++ b/test/tap/groups/ai/pgsql-seed.sql @@ -0,0 +1,24 @@ +CREATE TABLE IF NOT EXISTS public.tap_pgsql_static_accounts ( + account_id INT PRIMARY KEY, + account_name TEXT NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS public.tap_pgsql_static_events ( + event_id INT PRIMARY KEY, + account_id INT NOT NULL, + event_type TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT fk_tap_pgsql_events_account + FOREIGN KEY (account_id) REFERENCES public.tap_pgsql_static_accounts(account_id) +); + +INSERT INTO public.tap_pgsql_static_accounts(account_id, account_name) VALUES + (1, 'seed-pg-a'), + (2, 'seed-pg-b') +ON CONFLICT (account_id) DO UPDATE SET account_name=EXCLUDED.account_name; + +INSERT INTO public.tap_pgsql_static_events(event_id, account_id, event_type) VALUES + (201, 1, 'signup'), + (202, 2, 'purchase') +ON CONFLICT (event_id) DO UPDATE SET event_type=EXCLUDED.event_type; diff --git a/test/tap/groups/ai/pre-proxysql.bash b/test/tap/groups/ai/pre-proxysql.bash index bab19fd57..181ad786a 100755 --- a/test/tap/groups/ai/pre-proxysql.bash +++ b/test/tap/groups/ai/pre-proxysql.bash @@ -72,10 +72,26 @@ create_pgsql_monitor_user() { fi } +seed_mysql_test_data() { + echo "[INFO] AI pre-hook: seeding MySQL static-harvest test data" + mysql -h"${TAP_MYSQLHOST}" -P"${TAP_MYSQLPORT}" -u"${TAP_MYSQLUSERNAME}" -p"${TAP_MYSQLPASSWORD}" < "${SCRIPT_DIR}/mysql-seed.sql" +} + +seed_pgsql_test_data() { + echo "[INFO] AI pre-hook: seeding PostgreSQL static-harvest test data" + if command -v psql >/dev/null 2>&1; then + PGPASSWORD="${AI_PGSQL_PASSWORD}" psql -h "${AI_PGSQL_HOST}" -p "${AI_PGSQL_PORT}" -U "${AI_PGSQL_USER}" -d "${AI_PGSQL_DB}" -v ON_ERROR_STOP=1 -f "${SCRIPT_DIR}/pgsql-seed.sql" + else + compose exec -T pgsql psql -U "${AI_PGSQL_USER}" -d "${AI_PGSQL_DB}" -v ON_ERROR_STOP=1 < "${SCRIPT_DIR}/pgsql-seed.sql" + fi +} + echo "[INFO] AI pre-hook: starting group-local containers" "${SCRIPT_DIR}/docker-compose-init.bash" create_mysql_monitor_user create_pgsql_monitor_user +seed_mysql_test_data +seed_pgsql_test_data echo "[INFO] AI pre-hook: configuring ProxySQL MCP and backend routing" diff --git a/test/tap/tests/test_mcp_static_harvest-t.sh b/test/tap/tests/test_mcp_static_harvest-t.sh new file mode 100755 index 000000000..4d2fb18f2 --- /dev/null +++ b/test/tap/tests/test_mcp_static_harvest-t.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# +# test_mcp_static_harvest-t.sh +# +# TAP test for MCP static harvesting (phase A) across: +# - MySQL target_id +# - PostgreSQL target_id +# + +set -euo pipefail + +PLAN=8 +DONE=0 +FAIL=0 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HELPERS="${SCRIPT_DIR}/mcp_rules_testing/mcp_test_helpers.sh" + +if [[ ! -f "${HELPERS}" ]]; then + echo "msg: 1..1" + echo "msg: not ok 1 - missing helper ${HELPERS}" + exit 1 +fi +source "${HELPERS}" + +MCP_MYSQL_TARGET_ID="${MCP_TARGET_ID:-tap_mysql_default}" +MCP_PGSQL_TARGET_ID="${MCP_PGSQL_TARGET_ID:-tap_pgsql_default}" + +MYSQL_SEEDED_TABLE="${MYSQL_SEEDED_TABLE:-tap_mysql_static_customers}" +PGSQL_SEEDED_TABLE="${PGSQL_SEEDED_TABLE:-tap_pgsql_static_accounts}" + +MYSQL_RUN_ID="" +PGSQL_RUN_ID="" + +tap_ok() { + DONE=$((DONE + 1)) + echo "msg: ok ${DONE} - $1" +} + +tap_not_ok() { + DONE=$((DONE + 1)) + FAIL=$((FAIL + 1)) + echo "msg: not ok ${DONE} - $1" + if [[ $# -gt 1 ]]; then + echo "msg: # $2" + fi +} + +extract_run_id() { + local payload="$1" + echo "${payload}" | sed -n 's/.*"run_id"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -n1 +} + +echo "msg: 1..${PLAN}" +echo "msg: # MCP Static Harvest Test Suite" + +if check_proxysql_admin; then + tap_ok "ProxySQL admin reachable" +else + tap_not_ok "ProxySQL admin reachable" +fi + +if check_mcp_server; then + tap_ok "MCP server reachable" +else + tap_not_ok "MCP server reachable" +fi + +targets_resp="$(mcp_request "query" '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list_targets","arguments":{}},"id":1}')" +if echo "${targets_resp}" | grep -q "\"target_id\":\"${MCP_MYSQL_TARGET_ID}\"" && \ + echo "${targets_resp}" | grep -q "\"target_id\":\"${MCP_PGSQL_TARGET_ID}\""; then + tap_ok "list_targets contains mysql+pgsql target_id" +else + tap_not_ok "list_targets contains mysql+pgsql target_id" "${targets_resp}" +fi + +mysql_harvest_resp="$(mcp_request "query" "{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"params\":{\"name\":\"discovery.run_static\",\"arguments\":{\"target_id\":\"${MCP_MYSQL_TARGET_ID}\",\"schema_filter\":\"${MYSQL_DATABASE}\",\"notes\":\"tap mysql static harvest\"}},\"id\":2}")" +MYSQL_RUN_ID="$(extract_run_id "${mysql_harvest_resp}")" +if [[ -n "${MYSQL_RUN_ID}" ]] && echo "${mysql_harvest_resp}" | grep -q "\"target_id\":\"${MCP_MYSQL_TARGET_ID}\"" && echo "${mysql_harvest_resp}" | grep -q "\"protocol\":\"mysql\""; then + tap_ok "discovery.run_static mysql target returns run_id/protocol" +else + tap_not_ok "discovery.run_static mysql target returns run_id/protocol" "${mysql_harvest_resp}" +fi + +mysql_catalog_resp="$(mcp_request "query" "{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"params\":{\"name\":\"catalog.list_objects\",\"arguments\":{\"target_id\":\"${MCP_MYSQL_TARGET_ID}\",\"run_id\":\"${MYSQL_RUN_ID}\",\"object_type\":\"table\",\"schema_name\":\"${MYSQL_DATABASE}\",\"page_size\":200}},\"id\":3}")" +if echo "${mysql_catalog_resp}" | grep -q "\"object_name\":\"${MYSQL_SEEDED_TABLE}\""; then + tap_ok "catalog.list_objects mysql run exposes seeded mysql table" +else + tap_not_ok "catalog.list_objects mysql run exposes seeded mysql table" "${mysql_catalog_resp}" +fi + +pgsql_harvest_resp="$(mcp_request "query" "{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"params\":{\"name\":\"discovery.run_static\",\"arguments\":{\"target_id\":\"${MCP_PGSQL_TARGET_ID}\",\"schema_filter\":\"public\",\"notes\":\"tap pgsql static harvest\"}},\"id\":4}")" +PGSQL_RUN_ID="$(extract_run_id "${pgsql_harvest_resp}")" +if [[ -n "${PGSQL_RUN_ID}" ]] && echo "${pgsql_harvest_resp}" | grep -q "\"target_id\":\"${MCP_PGSQL_TARGET_ID}\"" && echo "${pgsql_harvest_resp}" | grep -q "\"protocol\":\"pgsql\""; then + tap_ok "discovery.run_static pgsql target returns run_id/protocol" +else + tap_not_ok "discovery.run_static pgsql target returns run_id/protocol" "${pgsql_harvest_resp}" +fi + +pgsql_catalog_resp="$(mcp_request "query" "{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"params\":{\"name\":\"catalog.list_objects\",\"arguments\":{\"target_id\":\"${MCP_PGSQL_TARGET_ID}\",\"run_id\":\"${PGSQL_RUN_ID}\",\"object_type\":\"table\",\"schema_name\":\"public\",\"page_size\":200}},\"id\":5}")" +if echo "${pgsql_catalog_resp}" | grep -q "\"object_name\":\"${PGSQL_SEEDED_TABLE}\""; then + tap_ok "catalog.list_objects pgsql run exposes seeded pgsql table" +else + tap_not_ok "catalog.list_objects pgsql run exposes seeded pgsql table" "${pgsql_catalog_resp}" +fi + +cross_target_resp="$(mcp_request "query" "{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"params\":{\"name\":\"catalog.list_objects\",\"arguments\":{\"target_id\":\"${MCP_PGSQL_TARGET_ID}\",\"run_id\":\"${MYSQL_RUN_ID}\",\"object_type\":\"table\",\"page_size\":10}},\"id\":6}")" +if echo "${cross_target_resp}" | grep -q "\"error\"" && echo "${cross_target_resp}" | grep -q "target_id"; then + tap_ok "catalog run_id cannot be resolved across target_id boundaries" +else + tap_not_ok "catalog run_id cannot be resolved across target_id boundaries" "${cross_target_resp}" +fi + +if [[ "${FAIL}" -ne 0 ]]; then + echo "msg: # FAILURES=${FAIL}/${PLAN}" + exit 1 +fi +exit 0