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.
pull/5386/head
Rene Cannao 3 months ago
parent 9685cdaa4b
commit 2538e303cf

@ -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.

@ -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);

@ -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;

@ -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"

@ -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
Loading…
Cancel
Save