Skip to content

Commit 3ce5f29

Browse files
authored
Merge pull request #240 from Hyperkid123/feat/RHCLOUD-48384
feat(ci): add Postgres sidecar for migration tests
2 parents 920fdaf + 9a484ed commit 3ce5f29

3 files changed

Lines changed: 286 additions & 0 deletions

File tree

.tekton/platform-frontend-ai-dev-memory-server-pull-request.yaml

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ spec:
3131
value: memory-server/Dockerfile
3232
- name: path-context
3333
value: .
34+
- name: PGSQL_USER
35+
value: devbot_test
36+
- name: PGSQL_PASSWORD
37+
value: devbot_test
38+
- name: PGSQL_HOSTNAME
39+
value: localhost
40+
- name: PGSQL_PORT
41+
value: '5432'
42+
- name: PGSQL_DATABASE
43+
value: devbot_migration_test
3444
pipelineSpec:
3545
description: |
3646
This pipeline is ideal for building container images from a Containerfile while reducing network traffic.
@@ -112,6 +122,16 @@ spec:
112122
type: string
113123
default: .
114124
description: Target directories to scan with SAST tools. Multiple values should be separated with commas.
125+
- name: PGSQL_USER
126+
type: string
127+
- name: PGSQL_PASSWORD
128+
type: string
129+
- name: PGSQL_HOSTNAME
130+
type: string
131+
- name: PGSQL_PORT
132+
type: string
133+
- name: PGSQL_DATABASE
134+
type: string
115135
results:
116136
- description: ""
117137
name: IMAGE_URL
@@ -161,6 +181,109 @@ spec:
161181
workspace: workspace
162182
- name: basic-auth
163183
workspace: git-auth
184+
- name: clone-repository-oci-ta
185+
params:
186+
- name: url
187+
value: $(params.git-url)
188+
- name: revision
189+
value: $(params.revision)
190+
- name: ociStorage
191+
value: $(params.output-image).git
192+
runAfter:
193+
- init
194+
taskRef:
195+
params:
196+
- name: name
197+
value: git-clone-oci-ta
198+
- name: bundle
199+
value: quay.io/konflux-ci/tekton-catalog/task-git-clone-oci-ta:0.1@sha256:d30f13dd15daf89dd6dc645243b3444d35570d13f7840c3fd65e366022515205
200+
- name: kind
201+
value: task
202+
resolver: bundles
203+
workspaces:
204+
- name: basic-auth
205+
workspace: git-auth
206+
- name: run-migration-tests
207+
params:
208+
- name: SOURCE_ARTIFACT
209+
value: $(tasks.clone-repository-oci-ta.results.SOURCE_ARTIFACT)
210+
- name: PGSQL_USER
211+
value: $(params.PGSQL_USER)
212+
- name: PGSQL_PASSWORD
213+
value: $(params.PGSQL_PASSWORD)
214+
- name: PGSQL_HOSTNAME
215+
value: $(params.PGSQL_HOSTNAME)
216+
- name: PGSQL_PORT
217+
value: $(params.PGSQL_PORT)
218+
- name: PGSQL_DATABASE
219+
value: $(params.PGSQL_DATABASE)
220+
runAfter:
221+
- clone-repository-oci-ta
222+
taskSpec:
223+
sidecars:
224+
- name: postgresql
225+
image: docker.io/pgvector/pgvector:pg15
226+
env:
227+
- name: POSTGRES_DB
228+
value: $(params.PGSQL_DATABASE)
229+
- name: POSTGRES_USER
230+
value: $(params.PGSQL_USER)
231+
- name: POSTGRES_PASSWORD
232+
value: $(params.PGSQL_PASSWORD)
233+
computeResources: {}
234+
params:
235+
- name: SOURCE_ARTIFACT
236+
type: string
237+
- name: PGSQL_USER
238+
type: string
239+
- name: PGSQL_PASSWORD
240+
type: string
241+
- name: PGSQL_HOSTNAME
242+
type: string
243+
- name: PGSQL_PORT
244+
type: string
245+
- name: PGSQL_DATABASE
246+
type: string
247+
volumes:
248+
- name: workdir
249+
emptyDir: {}
250+
stepTemplate:
251+
volumeMounts:
252+
- mountPath: /var/workdir
253+
name: workdir
254+
readOnly: false
255+
steps:
256+
- name: use-trusted-artifact
257+
image: quay.io/redhat-appstudio/build-trusted-artifacts:latest@sha256:9b180776a41d9a22a1c51539f1647c60defbbd55b44bbebdd4130e33512d8b0d
258+
args:
259+
- use
260+
- $(params.SOURCE_ARTIFACT)=/var/workdir
261+
- name: run-tests
262+
image: registry.access.redhat.com/ubi9/python-312:latest
263+
workingDir: /var/workdir/memory-server
264+
securityContext:
265+
runAsUser: 0
266+
script: |
267+
#!/bin/bash
268+
set -ex
269+
270+
echo "Waiting for PostgreSQL..."
271+
for i in $(seq 1 30); do
272+
if bash -c "echo > /dev/tcp/$(params.PGSQL_HOSTNAME)/$(params.PGSQL_PORT)" 2>/dev/null; then
273+
echo "PostgreSQL is ready"
274+
break
275+
fi
276+
echo "Attempt $i/30 — waiting..."
277+
sleep 2
278+
done
279+
280+
pip install --quiet asyncpg pytest pytest-asyncio
281+
PGSQL_HOSTNAME=$(params.PGSQL_HOSTNAME) \
282+
PGSQL_PORT=$(params.PGSQL_PORT) \
283+
PGSQL_USER=$(params.PGSQL_USER) \
284+
PGSQL_PASSWORD=$(params.PGSQL_PASSWORD) \
285+
PGSQL_DATABASE=$(params.PGSQL_DATABASE) \
286+
python -m pytest tests/test_db_migration.py -v -p no:cacheprovider
164287
- name: prefetch-dependencies
165288
params:
166289
- name: input

memory-server/tests/conftest.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import os
2+
from pathlib import Path
3+
4+
import asyncpg
5+
import pytest_asyncio
6+
7+
DB_CONFIG = {
8+
"host": os.getenv("PGSQL_HOSTNAME", "localhost"),
9+
"port": int(os.getenv("PGSQL_PORT", "5432")),
10+
"user": os.getenv("PGSQL_USER", "devbot_test"),
11+
"password": os.getenv("PGSQL_PASSWORD", "devbot_test"),
12+
"database": os.getenv("PGSQL_DATABASE", "devbot_migration_test"),
13+
}
14+
15+
SCHEMA_PATH = Path(__file__).parent.parent / "src" / "schema.sql"
16+
17+
18+
@pytest_asyncio.fixture
19+
async def db():
20+
conn = await asyncpg.connect(**DB_CONFIG)
21+
tables = await conn.fetch(
22+
"SELECT tablename FROM pg_tables WHERE schemaname = 'public'"
23+
)
24+
for t in tables:
25+
await conn.execute(f"DROP TABLE IF EXISTS {t['tablename']} CASCADE")
26+
await conn.execute("DROP EXTENSION IF EXISTS vector CASCADE")
27+
yield conn
28+
await conn.close()
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Schema validation tests against real PostgreSQL with pgvector.
2+
3+
These run in CI via the Tekton pipeline's Postgres sidecar.
4+
Future migration stages add tests here for ALTER TABLE + backfill scripts.
5+
"""
6+
7+
import asyncpg
8+
import pytest
9+
10+
from conftest import SCHEMA_PATH
11+
12+
13+
@pytest.mark.asyncio
14+
async def test_db_connection(db):
15+
result = await db.fetchval("SELECT 1")
16+
assert result == 1
17+
18+
19+
@pytest.mark.asyncio
20+
async def test_schema_applies(db):
21+
schema = SCHEMA_PATH.read_text()
22+
await db.execute(schema)
23+
24+
tables = await db.fetch(
25+
"SELECT tablename FROM pg_tables WHERE schemaname = 'public'"
26+
)
27+
table_names = {t["tablename"] for t in tables}
28+
29+
expected = {
30+
"tasks",
31+
"memories",
32+
"bot_status",
33+
"cycles",
34+
"cycle_runs",
35+
"slack_notifications",
36+
"bot_instances",
37+
}
38+
missing = expected - table_names
39+
assert not missing, f"Missing tables: {missing}"
40+
41+
42+
@pytest.mark.asyncio
43+
async def test_schema_idempotent(db):
44+
schema = SCHEMA_PATH.read_text()
45+
await db.execute(schema)
46+
await db.execute(schema)
47+
count = await db.fetchval("SELECT COUNT(*) FROM tasks")
48+
assert count == 0
49+
50+
51+
@pytest.mark.asyncio
52+
async def test_task_insert_read(db):
53+
schema = SCHEMA_PATH.read_text()
54+
await db.execute(schema)
55+
56+
await db.execute(
57+
"""
58+
INSERT INTO tasks (jira_key, status, repo, branch)
59+
VALUES ($1, $2, $3, $4)
60+
""",
61+
"TEST-1",
62+
"in_progress",
63+
"test-repo",
64+
"bot/TEST-1",
65+
)
66+
67+
task = await db.fetchrow("SELECT * FROM tasks WHERE jira_key = $1", "TEST-1")
68+
assert task is not None
69+
assert task["status"] == "in_progress"
70+
assert task["repo"] == "test-repo"
71+
assert task["branch"] == "bot/TEST-1"
72+
assert task["pr_number"] is None
73+
assert task["pr_url"] is None
74+
75+
76+
@pytest.mark.asyncio
77+
async def test_task_unique_constraint(db):
78+
schema = SCHEMA_PATH.read_text()
79+
await db.execute(schema)
80+
81+
await db.execute(
82+
"INSERT INTO tasks (jira_key, status, repo, branch) VALUES ($1, $2, $3, $4)",
83+
"DUP-1",
84+
"in_progress",
85+
"repo",
86+
"bot/DUP-1",
87+
)
88+
89+
with pytest.raises(asyncpg.UniqueViolationError):
90+
await db.execute(
91+
"INSERT INTO tasks (jira_key, status, repo, branch) VALUES ($1, $2, $3, $4)",
92+
"DUP-1",
93+
"pr_open",
94+
"repo2",
95+
"bot/DUP-1-2",
96+
)
97+
98+
99+
@pytest.mark.asyncio
100+
async def test_foreign_keys(db):
101+
schema = SCHEMA_PATH.read_text()
102+
await db.execute(schema)
103+
104+
await db.execute(
105+
"INSERT INTO tasks (jira_key, status, repo, branch) VALUES ($1, $2, $3, $4)",
106+
"FK-1",
107+
"in_progress",
108+
"repo",
109+
"bot/FK-1",
110+
)
111+
task_id = await db.fetchval("SELECT id FROM tasks WHERE jira_key = $1", "FK-1")
112+
113+
await db.execute(
114+
"""
115+
INSERT INTO cycle_runs (task_id, cycle_type, instance_id)
116+
VALUES ($1, $2, $3)
117+
""",
118+
task_id,
119+
"task_work",
120+
"test-instance",
121+
)
122+
cycle = await db.fetchrow("SELECT * FROM cycle_runs WHERE task_id = $1", task_id)
123+
assert cycle is not None
124+
assert cycle["cycle_type"] == "task_work"
125+
126+
with pytest.raises(asyncpg.ForeignKeyViolationError):
127+
await db.execute(
128+
"""
129+
INSERT INTO cycle_runs (task_id, cycle_type, instance_id)
130+
VALUES ($1, $2, $3)
131+
""",
132+
99999,
133+
"task_work",
134+
"test-instance",
135+
)

0 commit comments

Comments
 (0)