Skip to content

Commit 04f720d

Browse files
yyualvinjerry-ng2
andauthored
Integration tests for prefiltering based on jira issues (#51)
* Jira ticket status parser * chore: integration tests and modify utils.py to fetch jira link and jira resolve time from the events database * chore: move tests into test folder * fix: handle missing ticket columns in known issue filter; add uv.lock * chore: fix linting errors * chore: fix linting * chore: switch back to fetching previous issues from results table --------- Co-authored-by: jerry-ng2 <dnguyen122003@gmail.com>
1 parent 433c5cf commit 04f720d

11 files changed

Lines changed: 385 additions & 16 deletions

deploy/batch-rca-automation/docs/batch-rca-flow.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ flowchart TD
3636
direction TB
3737
F0{--no-pre-filter<br/>flag set?}
3838
F1[fetch_known_issues.py<br/>Query recent high-confidence results<br/>from last 4 hours]
39+
F1a["Exclude rows where result ticket<br/>closed >= 4h ago (ticket_resolve_datetime_gmt)"]
3940
F2{Known issues<br/>found?}
4041
F3["pre_filter_jobs.py<br/>(normal mode)"]
42+
F3a["Same active-ticket filter on results row<br/>(ticket_link, ticket_resolve_datetime_gmt)"]
4143
F4["Pass 1: Match on catalog_item<br/>+ error_message similarity >= 0.75"]
4244
F5["Pass 2: Cross-catalog match on<br/>error_message similarity >= 0.90<br/>(catches platform-wide failures)"]
4345
F6{Pre-matched<br/>jobs?}
@@ -47,9 +49,9 @@ flowchart TD
4749
F10[Skip pre-filter]
4850
4951
F0 -- Yes --> F10
50-
F0 -- No --> F1 --> F2
52+
F0 -- No --> F1 --> F1a --> F2
5153
F2 -- No --> F10
52-
F2 -- Yes --> F3 --> F4 --> F5 --> F6
54+
F2 -- Yes --> F3 --> F3a --> F4 --> F5 --> F6
5355
F6 -- No --> F10
5456
F6 -- Yes --> F7 --> F8
5557
F8 -- No, all matched --> F9 --> Done2([Exit 0])
@@ -117,7 +119,7 @@ flowchart TD
117119
ST4["Validate match:<br/>id + root_cause_category<br/>+ confidence = high"]
118120
ST5{Valid?}
119121
ST6["Use matched FK<br/>Update source: FK + ai_processed"]
120-
ST7["Fallback: find_match()<br/>difflib similarity >= 0.85<br/>same catalog_item + category"]
122+
ST7["Fallback: find_match()<br/>difflib similarity >= 0.85<br/>same catalog_item + category<br/>+ excludes closed tickets on results row (>=4h)"]
121123
ST8{Match found?}
122124
ST9["INSERT into results table<br/>Update source: FK + ai_processed"]
123125
@@ -155,4 +157,6 @@ flowchart TD
155157
style PerAgent fill:#e2d6f3
156158
style Step5 fill:#d4edda
157159
style Step5b fill:#d4edda
160+
style F1a fill:#ffe0b2
161+
style F3a fill:#ffe0b2
158162
```

deploy/batch-rca-automation/scripts/fetch_known_issues.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import psycopg2
1212
import psycopg2.sql
13-
from utils import connect_db, load_config
13+
from utils import connect_db, known_issue_active_sql, load_config
1414

1515

1616
def fetch_known_issues(
@@ -33,11 +33,15 @@ def fetch_known_issues(
3333
WHERE confidence = 'high'
3434
AND batch_id >= %s
3535
AND status = 'analyzed'
36+
AND {active}
3637
ORDER BY root_cause_category, catalog_item, batch_id DESC
3738
) sub
3839
ORDER BY batch_id DESC
3940
LIMIT %s"""
40-
).format(psycopg2.sql.Identifier(results_table)),
41+
).format(
42+
psycopg2.sql.Identifier(results_table),
43+
active=known_issue_active_sql(conn, table=results_table),
44+
),
4145
(cutoff_batch_id, limit),
4246
)
4347
rows = cur.fetchall()

deploy/batch-rca-automation/scripts/pre_filter_jobs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import psycopg2
2323
import psycopg2.sql
24-
from utils import connect_db, load_config
24+
from utils import connect_db, known_issue_active_sql, load_config
2525

2626
MATCH_THRESHOLD = 0.75
2727
CROSS_CATALOG_THRESHOLD = 0.90
@@ -76,10 +76,12 @@ def fetch_filter_context(
7676
WHERE r.confidence = 'high'
7777
AND r.batch_id >= %s
7878
AND r.status = 'analyzed'
79+
AND {active}
7980
ORDER BY r.batch_id DESC"""
8081
).format(
8182
results=psycopg2.sql.Identifier(results_table),
8283
source=psycopg2.sql.Identifier(source_table),
84+
active=known_issue_active_sql(conn, alias="r", table=results_table),
8385
),
8486
(cutoff_batch_id,),
8587
)

deploy/batch-rca-automation/scripts/query_historical_matches.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import psycopg2
1515
import psycopg2.sql
16-
from utils import connect_db, load_config
16+
from utils import connect_db, known_issue_active_sql, load_config
1717

1818

1919
def query_matches(
@@ -44,8 +44,9 @@ def query_matches(
4444
" AND (root_cause_category, catalog_item) IN (" + in_clause + ")"
4545
" AND confidence IN ('high', 'medium')"
4646
" AND status = 'analyzed'"
47+
" AND {active}"
4748
" ORDER BY batch_id DESC"
48-
).format(psycopg2.sql.Identifier(table))
49+
).format(psycopg2.sql.Identifier(table), active=known_issue_active_sql(conn, table=table))
4950

5051
params: list[Any] = [cutoff_batch_id]
5152
if exclude_batch:
@@ -57,8 +58,9 @@ def query_matches(
5758
" AND (root_cause_category, catalog_item) IN (" + in_clause + ")"
5859
" AND confidence IN ('high', 'medium')"
5960
" AND status = 'analyzed'"
61+
" AND {active}"
6062
" ORDER BY batch_id DESC"
61-
).format(psycopg2.sql.Identifier(table))
63+
).format(psycopg2.sql.Identifier(table), active=known_issue_active_sql(conn, table=table))
6264
params = [cutoff_batch_id, exclude_batch]
6365

6466
params.extend(tuple_values)

deploy/batch-rca-automation/scripts/store_report.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import psycopg2
1515
import psycopg2.sql
16-
from utils import connect_db, load_config
16+
from utils import connect_db, known_issue_active_sql, load_config
1717

1818
MATCH_THRESHOLD = 0.85
1919
LOOKBACK_HOURS = 4
@@ -27,8 +27,12 @@ def find_match(cur: Any, results_table: str, job: dict[str, Any]) -> int | None:
2727
psycopg2.sql.SQL(
2828
"""SELECT id, root_cause_summary FROM {}
2929
WHERE root_cause_category = %s AND catalog_item = %s
30-
AND confidence = 'high' AND batch_id >= %s"""
31-
).format(psycopg2.sql.Identifier(results_table)),
30+
AND confidence = 'high' AND batch_id >= %s
31+
AND {active}"""
32+
).format(
33+
psycopg2.sql.Identifier(results_table),
34+
active=known_issue_active_sql(cur.connection, table=results_table),
35+
),
3236
(job.get("root_cause_category"), job.get("catalog_item"), cutoff_batch_id),
3337
)
3438
summary = job.get("root_cause_summary", "")
@@ -104,8 +108,8 @@ def store_report(
104108
psycopg2.sql.SQL(
105109
"""INSERT INTO {}
106110
(batch_id, job_id, status, root_cause_category, root_cause_summary,
107-
confidence, catalog_item, job_duration_seconds, ticket_link, is_open)
108-
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
111+
confidence, catalog_item, job_duration_seconds)
112+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
109113
ON CONFLICT (batch_id, job_id) DO UPDATE SET status = EXCLUDED.status
110114
RETURNING id"""
111115
).format(psycopg2.sql.Identifier(results_table)),
@@ -118,8 +122,6 @@ def store_report(
118122
job.get("confidence"),
119123
job.get("catalog_item"),
120124
job.get("job_duration_seconds"),
121-
job.get("ticket_link"),
122-
job.get("is_open", False),
123125
),
124126
)
125127
row = cur.fetchone()

deploy/batch-rca-automation/scripts/utils.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import psycopg2
1010
import psycopg2.extras
11+
import psycopg2.sql
1112
from dotenv import load_dotenv
1213

1314
ALL_CONFIG_KEYS = {
@@ -20,6 +21,36 @@
2021
"results_table": ("SOURCE_DB_RESULT_TABLE", ""),
2122
}
2223

24+
# Grace period after a linked JIRA ticket is closed before a known-issue row
25+
# is excluded from matching. Distinct from the unrelated lookback_hours=4
26+
# recency windows used elsewhere in this pipeline.
27+
TICKET_CLOSED_GRACE_HOURS = 4
28+
29+
_TICKET_COLUMNS = ("ticket_link", "ticket_resolve_datetime_gmt")
30+
31+
# Per-table cache so the schema is only probed once per process, not once per query.
32+
_ticket_columns_present: dict[str, bool] = {}
33+
34+
35+
def _has_ticket_columns(conn: Any, table: str) -> bool:
36+
if table not in _ticket_columns_present:
37+
with conn.cursor(cursor_factory=psycopg2.extensions.cursor) as cur:
38+
cur.execute(
39+
"SELECT column_name FROM information_schema.columns"
40+
" WHERE table_name = %s AND column_name = ANY(%s)",
41+
(table, list(_TICKET_COLUMNS)),
42+
)
43+
found = {row[0] for row in cur.fetchall()}
44+
present = set(_TICKET_COLUMNS) <= found
45+
if not present:
46+
print(
47+
f"[WARN] {table} is missing {', '.join(_TICKET_COLUMNS)}; "
48+
"known-issue ticket filtering disabled, treating all known issues as active",
49+
file=sys.stderr,
50+
)
51+
_ticket_columns_present[table] = present
52+
return _ticket_columns_present[table]
53+
2354

2455
def load_config(required: tuple[str, ...] = ("name", "user", "password")) -> dict[str, Any]:
2556
env_file = os.path.join(os.path.dirname(__file__), "..", ".env")
@@ -39,6 +70,45 @@ def load_config(required: tuple[str, ...] = ("name", "user", "password")) -> dic
3970
return config
4071

4172

73+
def known_issue_active_sql(
74+
conn: Any,
75+
alias: str = "",
76+
*,
77+
table: str,
78+
) -> psycopg2.sql.Composable:
79+
"""Boolean SQL fragment (no leading AND): TRUE unless the row's linked
80+
ticket is closed and has been for TICKET_CLOSED_GRACE_HOURS+.
81+
82+
ticket_link and ticket_resolve_datetime_gmt live directly on the row
83+
being filtered (the results table).
84+
85+
A row is excluded only when ticket_link is set and
86+
ticket_resolve_datetime_gmt is old enough. Every other combination (no
87+
ticket, or an unknown/recent resolve time) is active.
88+
89+
If the results table is missing ticket_link/ticket_resolve_datetime_gmt
90+
(schema drift), ticket filtering is disabled and every row is treated as
91+
active -- a warning is printed once per table.
92+
"""
93+
94+
def col(name: str) -> psycopg2.sql.Identifier:
95+
return psycopg2.sql.Identifier(alias, name) if alias else psycopg2.sql.Identifier(name)
96+
97+
if not table:
98+
raise ValueError("table is required for known_issue_active_sql")
99+
100+
if not _has_ticket_columns(conn, table):
101+
return psycopg2.sql.SQL("TRUE")
102+
103+
return psycopg2.sql.SQL(
104+
"({tl} IS NULL OR {rd} IS NULL OR {rd} > NOW() - make_interval(hours => {h}))"
105+
).format(
106+
tl=col("ticket_link"),
107+
rd=col("ticket_resolve_datetime_gmt"),
108+
h=psycopg2.sql.Literal(TICKET_CLOSED_GRACE_HOURS),
109+
)
110+
111+
42112
def connect_db(config: dict[str, Any], *, use_dict_cursor: bool = False) -> Any:
43113
kwargs: dict[str, Any] = {
44114
"host": config["host"],
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Postgres fixtures for batch-rca-automation integration tests."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
import sys
7+
from collections.abc import Generator
8+
from pathlib import Path
9+
10+
import psycopg2
11+
import pytest
12+
13+
SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts"
14+
sys.path.insert(0, str(SCRIPTS_DIR))
15+
16+
SOURCE_TABLE = "aap2_events"
17+
RESULTS_TABLE = "aap2_job_results"
18+
19+
SCHEMA_SQL = f"""
20+
DROP TABLE IF EXISTS {RESULTS_TABLE};
21+
DROP TABLE IF EXISTS {SOURCE_TABLE};
22+
23+
CREATE TABLE {SOURCE_TABLE} (
24+
job_id BIGINT PRIMARY KEY,
25+
job_finished TIMESTAMPTZ
26+
);
27+
28+
CREATE TABLE {RESULTS_TABLE} (
29+
id SERIAL PRIMARY KEY,
30+
job_id BIGINT NOT NULL,
31+
batch_id TEXT NOT NULL,
32+
confidence TEXT NOT NULL,
33+
status TEXT NOT NULL,
34+
catalog_item TEXT,
35+
root_cause_category TEXT,
36+
ticket_link TEXT,
37+
ticket_resolve_datetime_gmt TIMESTAMPTZ
38+
);
39+
"""
40+
41+
42+
def _db_kwargs() -> dict[str, object]:
43+
return {
44+
"host": os.environ.get("TEST_DB_HOST", "localhost"),
45+
"port": int(os.environ.get("TEST_DB_PORT", "5433")),
46+
"dbname": os.environ.get("TEST_DB_NAME", "rca_test"),
47+
"user": os.environ.get("TEST_DB_USER", "rca_test"),
48+
"password": os.environ.get("TEST_DB_PASSWORD", "rca_test"),
49+
}
50+
51+
52+
@pytest.fixture(scope="session")
53+
def db_conn() -> Generator[psycopg2.extensions.connection, None, None]:
54+
try:
55+
conn = psycopg2.connect(**_db_kwargs())
56+
except psycopg2.OperationalError as exc:
57+
pytest.skip(
58+
"Postgres test database not available. "
59+
"Start it with: docker compose -f docker-compose.test.yml up -d --wait"
60+
f" ({exc})"
61+
)
62+
63+
os.environ["SOURCE_DB_TABLE"] = SOURCE_TABLE
64+
65+
with conn.cursor() as cur:
66+
cur.execute(SCHEMA_SQL)
67+
conn.commit()
68+
69+
yield conn
70+
conn.close()
71+
72+
73+
@pytest.fixture
74+
def db(
75+
db_conn: psycopg2.extensions.connection,
76+
) -> Generator[psycopg2.extensions.connection, None, None]:
77+
with db_conn.cursor() as cur:
78+
cur.execute(f"TRUNCATE {RESULTS_TABLE} RESTART IDENTITY CASCADE")
79+
cur.execute(f"TRUNCATE {SOURCE_TABLE} RESTART IDENTITY CASCADE")
80+
db_conn.commit()
81+
yield db_conn
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
services:
2+
postgres:
3+
image: postgres:16-alpine
4+
environment:
5+
POSTGRES_USER: rca_test
6+
POSTGRES_PASSWORD: rca_test
7+
POSTGRES_DB: rca_test
8+
ports:
9+
- "5433:5432"
10+
healthcheck:
11+
test: ["CMD-SHELL", "pg_isready -U rca_test -d rca_test"]
12+
interval: 2s
13+
timeout: 5s
14+
retries: 15
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-r ../requirements.txt
2+
pytest>=8.0
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
COMPOSE_FILE="$TESTS_DIR/docker-compose.test.yml"
6+
7+
export TEST_DB_HOST="${TEST_DB_HOST:-localhost}"
8+
export TEST_DB_PORT="${TEST_DB_PORT:-5433}"
9+
export TEST_DB_NAME="${TEST_DB_NAME:-rca_test}"
10+
export TEST_DB_USER="${TEST_DB_USER:-rca_test}"
11+
export TEST_DB_PASSWORD="${TEST_DB_PASSWORD:-rca_test}"
12+
export SOURCE_DB_TABLE="${SOURCE_DB_TABLE:-aap2_events}"
13+
14+
cleanup() {
15+
docker compose -f "$COMPOSE_FILE" down -v >/dev/null 2>&1 || true
16+
}
17+
18+
if [[ "${KEEP_TEST_DB:-}" != "1" ]]; then
19+
trap cleanup EXIT
20+
fi
21+
22+
echo "[INFO] Starting Postgres test container..."
23+
docker compose -f "$COMPOSE_FILE" up -d --wait
24+
25+
echo "[INFO] Running integration tests..."
26+
python3 -m pytest "$TESTS_DIR" -v "$@"

0 commit comments

Comments
 (0)