@@ -37,6 +37,7 @@ def test_has_free_tier_job(self):
3737 def test_has_integration_postgres_job(self):
3838 self.assertIn("integration-postgres", self.jobs)
3939
40+ def test_has_windows_postgres_job(self):
4041 self.assertIn("windows-postgres", self.jobs)
4142
4243 def test_triggers_on_pull_request(self):
@@ -50,32 +51,129 @@ def test_triggers_on_push_to_main(self):
5051 self.assertIn("main", push_branches)
5152
5253
54+ class TestWindowsPostgresJob(unittest.TestCase):
55+ """The windows-postgres job must have all required steps and config."""
5356
54- class TestAllJobsHaveConsistentStructure(unittest.TestCase):
55- """Every job must have checkout + install-deps. Python jobs additionally
56- need Python setup and an artifact upload; Node jobs need Node setup.
57+ @classmethod
58+ def setUpClass(cls):
59+ if not WORKFLOW.exists():
60+ raise AssertionError(f"Workflow file not found: {WORKFLOW}")
61+ wf = _load_workflow()
62+ cls.job = wf.get("jobs", {}).get("windows-postgres", {})
63+ cls.step_names = [
64+ s.get("name", "") for s in cls.job.get("steps", [])
65+ ]
66+
67+ def test_runs_on_windows(self):
68+ self.assertEqual(self.job.get("runs-on"), "windows-latest")
69+
70+ def test_sets_pg_env_vars(self):
71+ env = self.job.get("env", {})
72+ for var in ("PGHOST", "PGPORT", "PGUSER", "PGPASSWORD", "PGDATABASE"):
73+ self.assertIn(var, env, f"Missing env var: {var}")
74+
75+ def test_has_checkout_step(self):
76+ self.assertTrue(
77+ any("Checkout" in n for n in self.step_names),
78+ "Missing Checkout step",
79+ )
80+
81+ def test_has_python_setup_step(self):
82+ self.assertTrue(
83+ any("Setup Python" in n for n in self.step_names),
84+ "Missing Setup Python step",
85+ )
86+
87+ def test_has_postgres_start_step(self):
88+ self.assertTrue(
89+ any("PostgreSQL" in n for n in self.step_names),
90+ "Missing PostgreSQL start/verify step",
91+ )
92+
93+ def test_has_database_config_step(self):
94+ self.assertTrue(
95+ any("config" in n.lower() for n in self.step_names),
96+ "Missing CI database config step",
97+ )
98+
99+ def test_has_create_databases_step(self):
100+ self.assertTrue(
101+ any("database" in n.lower() for n in self.step_names),
102+ "Missing create environment databases step",
103+ )
104+
105+ def test_has_evals_step(self):
106+ self.assertTrue(
107+ any("Eval" in n for n in self.step_names),
108+ "Missing evals step",
109+ )
110+
111+ def test_has_deploy_step(self):
112+ self.assertTrue(
113+ any("Deploy" in n for n in self.step_names),
114+ "Missing deploy environments step",
115+ )
116+
117+ def test_has_test_suite_step(self):
118+ self.assertTrue(
119+ any("test suite" in n.lower() or "test_report" in n.lower()
120+ for n in self.step_names),
121+ "Missing full test suite step",
122+ )
123+
124+ def test_has_upload_artifacts_step(self):
125+ self.assertTrue(
126+ any("Upload" in n for n in self.step_names),
127+ "Missing upload eval reports step",
128+ )
129+
130+ def test_postgres_step_uses_pwsh(self):
131+ pg_steps = [
132+ s for s in self.job.get("steps", [])
133+ if "PostgreSQL" in s.get("name", "")
134+ ]
135+ self.assertTrue(len(pg_steps) > 0)
136+ self.assertEqual(pg_steps[0].get("shell"), "pwsh")
137+
138+ def test_postgres_step_uses_runner_temp(self):
139+ pg_steps = [
140+ s for s in self.job.get("steps", [])
141+ if "PostgreSQL" in s.get("name", "")
142+ ]
143+ self.assertTrue(len(pg_steps) > 0)
144+ run_content = pg_steps[0].get("run", "")
145+ self.assertIn("RUNNER_TEMP", run_content,
146+ "Must use $RUNNER_TEMP for writable data directory")
147+
148+ def test_postgres_step_uses_initdb(self):
149+ pg_steps = [
150+ s for s in self.job.get("steps", [])
151+ if "PostgreSQL" in s.get("name", "")
152+ ]
153+ self.assertTrue(len(pg_steps) > 0)
154+ run_content = pg_steps[0].get("run", "")
155+ self.assertIn("initdb", run_content,
156+ "Must initialise data directory with initdb")
157+
158+ def test_postgres_step_uses_pg_ctl(self):
159+ pg_steps = [
160+ s for s in self.job.get("steps", [])
161+ if "PostgreSQL" in s.get("name", "")
162+ ]
163+ self.assertTrue(len(pg_steps) > 0)
164+ run_content = pg_steps[0].get("run", "")
165+ self.assertIn("pg_ctl", run_content,
166+ "Must start PostgreSQL with pg_ctl")
57167
58- BUG-023 added the Node-backed ``frontend-build`` job, so the old
59- "every job needs Python" contract needed to split by job kind. Any new
60- job MUST be classified in PYTHON_JOBS or NODE_JOBS below — an
61- unclassified job fails setUpClass, so a contributor can't quietly bypass
62- the structural rules.
63- """
64168
65- PYTHON_JOBS = {"free-tier", "integration-postgres", "windows-postgres"}
66- NODE_JOBS = {"frontend-build"}
169+ class TestAllJobsHaveConsistentStructure(unittest.TestCase):
170+ """All jobs must have checkout, python setup, and artifact upload."""
67171
68172 @classmethod
69173 def setUpClass(cls):
70174 if not WORKFLOW.exists():
71175 raise AssertionError(f"Workflow file not found: {WORKFLOW}")
72176 cls.jobs = _load_workflow().get("jobs", {})
73- unclassified = set(cls.jobs) - cls.PYTHON_JOBS - cls.NODE_JOBS
74- if unclassified:
75- raise AssertionError(
76- f"quality-gate.yml has unclassified jobs: {sorted(unclassified)}. "
77- f"Add each to PYTHON_JOBS or NODE_JOBS in {__file__}."
78- )
79177
80178 def _step_names(self, job_id: str) -> list[str]:
81179 return [s.get("name", "") for s in self.jobs[job_id].get("steps", [])]
@@ -88,44 +186,28 @@ def test_all_jobs_have_checkout(self):
88186 f"Job {job_id!r} missing Checkout step",
89187 )
90188
91- def test_all_jobs_install_deps(self):
92- # "Install dependencies" (Node) and "Install dev dependencies" (Python)
93- # both match: literal "Install" plus lowercase "dep".
189+ def test_all_jobs_have_python_setup(self):
94190 for job_id in self.jobs:
95191 names = self._step_names(job_id)
96- self.assertTrue(
97- any("Install" in n and "dep" in n.lower() for n in names),
98- f"Job {job_id!r} missing install dependencies step",
99- )
100-
101- def test_python_jobs_have_python_setup(self):
102- for job_id in self.PYTHON_JOBS:
103- if job_id not in self.jobs:
104- continue
105- names = self._step_names(job_id)
106192 self.assertTrue(
107193 any("Setup Python" in n or "Python" in n for n in names),
108- f"Python job {job_id!r} missing Python setup step",
194+ f"Job {job_id!r} missing Python setup step",
109195 )
110196
111- def test_python_jobs_have_artifact_upload(self):
112- for job_id in self.PYTHON_JOBS:
113- if job_id not in self.jobs:
114- continue
197+ def test_all_jobs_have_artifact_upload(self):
198+ for job_id in self.jobs:
115199 names = self._step_names(job_id)
116200 self.assertTrue(
117201 any("Upload" in n for n in names),
118- f"Python job {job_id!r} missing artifact upload step",
202+ f"Job {job_id!r} missing artifact upload step",
119203 )
120204
121- def test_node_jobs_have_node_setup(self):
122- for job_id in self.NODE_JOBS:
123- if job_id not in self.jobs:
124- continue
205+ def test_all_jobs_install_dev_deps(self):
206+ for job_id in self.jobs:
125207 names = self._step_names(job_id)
126208 self.assertTrue(
127- any("Setup Node " in n or "Node " in n for n in names),
128- f"Node job {job_id!r} missing Node setup step",
209+ any("Install " in n and "dep " in n.lower() for n in names),
210+ f"Job {job_id!r} missing install dev dependencies step",
129211 )
130212
131213
0 commit comments