← Files FactIQARCHIVED FILE
tests/test_sql_docs.py
11.8 KB · Nov 2, 2026 · 12:30 UTC
import json
import re
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / "scripts"
SKILL = (ROOT / "skills/factiq/SKILL.md").read_text(encoding="utf-8")
SQL_GUIDE = (ROOT / "references/data/sql-guide.md").read_text(encoding="utf-8")
CHART_SPEC = (ROOT / "references/output/chart-spec.md").read_text(encoding="utf-8")
REPORT_RULES = (ROOT / "references/report-patterns/README.md").read_text(
encoding="utf-8"
)
JOIN = "date_trunc('month', prior.time) = date_trunc('month', cur.time) - interval '1 year'"
YOY_TRAP = SQL_GUIDE.split("## Year-over-year on monthly data", 1)[1].split(
"## HS trade datasets", 1
)[0]
def normalized(text: str) -> str:
return " ".join(text.lower().split())
class PeriodAlignmentDocumentationTests(unittest.TestCase):
"""A row offset is not a period offset once a month is missing; the docs
must steer every year-over-year calculation to a date match."""
def test_sql_guide_states_time_is_the_period_start(self):
conventions = SQL_GUIDE.split("## Always-true conventions", 1)[1].split(
"\n## ", 1
)[0]
self.assertIn(
"**Match periods on the calendar month, never on the exact date.**",
conventions,
)
self.assertIn(JOIN, conventions)
def test_sql_guide_trap_shows_lag_as_bad_and_date_join_as_good(self):
bad, good = YOY_TRAP.split("Good", 1)
self.assertIn("Bad", bad)
self.assertIn("LAG(value, 12) OVER (ORDER BY time)", bad)
self.assertIn(JOIN, good)
self.assertNotIn("prior.time = cur.time - interval", good)
self.assertIn("date_trunc('quarter', ...)", good)
self.assertIn("generate_series(", good)
self.assertIn("ON date_trunc('month', d.time) = s.time", good)
self.assertIn('transform="yoy_pct"', good)
self.assertIn("series_math.py yoy", good)
def test_sql_guide_trap_tells_the_model_to_disclose_gaps(self):
text = normalized(YOY_TRAP)
self.assertIn("coverage_note", text)
self.assertIn("missing_periods", text)
self.assertIn("partial", text)
self.assertIn("blank", text)
def test_sql_guide_pivot_says_a_missing_period_is_absent_not_null(self):
pivot = SQL_GUIDE.split("## Pivoting to wide format", 1)[1].split(
"\n## ", 1
)[0]
self.assertIn("absent from the pivot, not a null", pivot)
def test_skill_get_series_row_documents_transform(self):
row = next(line for line in SKILL.splitlines() if line.startswith("| `get_series`"))
self.assertIn("`transform?`", row)
self.assertIn('transform="yoy_pct"', row)
self.assertIn('"yoy_diff"', row)
self.assertIn("matched by calendar date", row)
self.assertIn("`coverage_note`", row)
def test_skill_workflow_and_subagent_template_forbid_row_offsets(self):
compute_step = SKILL.split("**Compute deterministically.**", 1)[1].split(
"\n5. ", 1
)[0]
self.assertIn("matched by date", compute_step)
self.assertIn(JOIN, compute_step)
self.assertIn("never compare exact", compute_step)
self.assertIn("never `LAG(value, 12)`", compute_step)
self.assertIn("`missing_periods`", compute_step)
template = SKILL.split("You are a FactIQ research agent", 1)[1].split(
"FINDINGS:", 1
)[0]
self.assertIn("never a 12-row offset", template)
self.assertIn("coverage_note", template)
def test_skill_errors_section_explains_coverage_note(self):
errors = SKILL.split("## Errors and limits", 1)[1].split("\n## ", 1)[0]
self.assertIn("**`coverage_note` on a result**", errors)
self.assertIn("not an error", errors)
self.assertIn("matching dates", errors)
def test_report_rules_disclose_missing_periods(self):
rules = REPORT_RULES.split("## Rules the method implies", 1)[1]
self.assertIn("**A missing period is disclosed, not bridged.**", rules)
self.assertIn("`coverage_note`", rules)
def test_no_doc_endorses_a_row_offset_yoy(self):
# `shift(12)` / a bare LAG-by-12 may appear only inside the "Bad" example
# or in text that names it as the wrong pattern.
for name, text in (
("SKILL.md", SKILL),
("chart-spec.md", CHART_SPEC),
("README.md", REPORT_RULES),
):
with self.subTest(doc=name):
self.assertNotIn(".shift(12)", text)
self.assertNotIn("shift(12)", CHART_SPEC)
for match in re.finditer(r"LAG\(value, 12\)", SQL_GUIDE):
window = SQL_GUIDE[max(0, match.start() - 600) : match.start()]
self.assertTrue(
"Bad" in window or "spine" in window or "never" in window,
msg=SQL_GUIDE[match.start() - 120 : match.end() + 40],
)
def test_chart_spec_lineage_uses_a_date_matched_calculation(self):
calc = CHART_SPEC.split('"id": "calc"', 1)[1].split("}", 1)[0]
self.assertIn("series_math.py yoy", calc)
self.assertNotIn("12-month difference", calc)
class ChartSpecRequiredKeysTests(unittest.TestCase):
"""The renderer reads no `id` and no y-axis key, so the docs must not
require `id` and must name one y-axis label form."""
def test_id_is_not_a_required_key(self):
required = CHART_SPEC.split("Required:", 1)[1].split("\n\n", 1)[0]
self.assertNotIn("`id`", required)
self.assertNotIn("`id`", SKILL.split("required keys are", 1)[1].split(")", 1)[0])
def test_y_axis_label_form_is_stated(self):
optional = CHART_SPEC.split("Optional:", 1)[1].split("\n\n", 1)[0]
self.assertIn("`yAxisLabel`", optional)
self.assertIn("not as a\n`yAxis` object", optional)
def monthly_payload(start_year: int, end_year: int, missing: str) -> dict:
rows = []
level = 100.0
for year in range(start_year, end_year + 1):
for month in range(1, 13):
time = f"{year}-{month:02d}-01"
level = round(level * 1.005, 4)
if time != missing:
rows.append([time, level])
return {"columns": ["time", "value"], "results": rows}
def run_yoy(payload: dict, *extra: str) -> subprocess.CompletedProcess:
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "series.json"
path.write_text(json.dumps(payload), encoding="utf-8")
return subprocess.run(
[sys.executable, str(SCRIPTS / "series_math.py"), "yoy", "--file", str(path), *extra],
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
class SeriesMathYoyGapTests(unittest.TestCase):
"""`yoy` matches on the calendar period, so a hole never shifts the
comparison, and the periods with no prior-year value are named on stderr."""
def parse(self, stdout: str) -> dict[str, str]:
lines = stdout.strip().splitlines()
self.assertEqual(lines[0].split("\t"), ["time", "value", "yoy_pct"])
return {parts[0]: parts[2] for parts in (line.split("\t") for line in lines[1:])}
def test_month_after_a_hole_still_compares_with_the_same_month_prior_year(self):
for missing in ("2025-10-01", "2025-03-01"):
with self.subTest(missing=missing):
payload = monthly_payload(2024, 2026, missing)
result = run_yoy(payload)
self.assertEqual(result.returncode, 0, result.stderr)
yoy = self.parse(result.stdout)
# Every month after the hole must still be +6.17% on the
# compounding fixture (1.005**12 - 1), which is only true when
# the comparison is date-keyed; a 12-row offset gives 1.005**13.
after_hole = [
time for time in yoy if time > missing and time < "2026-01-01"
]
self.assertTrue(after_hole)
for time in after_hole:
self.assertAlmostEqual(float(yoy[time]), 6.1678, places=3, msg=time)
# The month one year after the hole has no prior value.
year, rest = missing.split("-", 1)
blank = f"{int(year) + 1}-{rest}"
self.assertEqual(yoy[blank], "")
self.assertNotIn(missing, yoy)
self.assertIn("no prior-year observation for " + blank, result.stderr)
self.assertIn("say so in the answer", result.stderr)
def test_complete_series_prints_no_gap_note(self):
result = run_yoy(monthly_payload(2024, 2026, missing=""))
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stderr, "")
yoy = self.parse(result.stdout)
self.assertEqual(yoy["2024-06-01"], "") # first year: no prior by design
self.assertNotEqual(yoy["2025-06-01"], "")
def test_gap_note_names_the_group_when_grouped(self):
payload = monthly_payload(2024, 2025, "2024-05-01")
payload["columns"] = ["series_id", "time", "value"]
payload["results"] = [["CPI", *row] for row in payload["results"]]
result = run_yoy(payload, "--group-col", "series_id")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("CPI: 2025-05-01", result.stderr)
class CompanyScreenerDocumentationTests(unittest.TestCase):
"""A question such as "technology companies with price-to-sales above 10"
is answered by one query on the view screener.companies; every doc that
routes questions must point there, and the guide must carry the rules
that change the answer."""
SCHEMAS = (ROOT / "references/data/schemas.md").read_text(encoding="utf-8")
SECTION = SQL_GUIDE.split("## Company screener", 1)[1].split(
"## Pivoting to wide format", 1
)[0]
def test_schema_table_lists_the_screener_and_points_to_the_guide(self):
row = next(
line for line in self.SCHEMAS.splitlines() if line.startswith("| `screener` |")
)
self.assertIn("`screener.companies`", row)
self.assertIn("`price_as_of`", row)
self.assertIn("references/data/sql-guide.md", row)
def test_routing_list_sends_condition_questions_to_the_view(self):
routing = normalized(self.SCHEMAS)
self.assertIn("companies that match conditions", routing)
self.assertIn("`run_sql` query on `screener.companies`", routing)
def test_skill_workflow_names_the_view_and_the_guide_section(self):
skill = normalized(SKILL)
self.assertIn("`screener.companies`", skill)
self.assertIn("**company screener**", skill)
def test_guide_lists_the_columns_a_condition_needs(self):
for column in (
"sector",
"market_cap",
"price_to_sales",
"price_to_earnings",
"enterprise_value",
"revenue_ttm",
"revenue_growth_yoy",
"reporting_currency",
"price_as_of",
"fundamentals_period_end",
):
self.assertIn(f"`{column}`", self.SECTION)
def test_guide_example_filters_the_view_and_limits_the_result(self):
example = self.SECTION.split("```sql", 1)[1].split("```", 1)[0]
self.assertIn("FROM screener.companies", example)
self.assertIn("ORDER BY", example)
self.assertIn("LIMIT", example)
def test_guide_states_the_rules_that_change_the_answer(self):
rules = normalized(self.SECTION)
self.assertIn("select sector, count(*) from screener.companies", rules)
self.assertIn("a ratio is null when its bottom number is zero or negative", rules)
self.assertIn("`reporting_currency` is not usd", rules)
self.assertIn("delayed about fifteen minutes", rules)
self.assertIn("state `price_as_of` in the answer", rules)
self.assertIn("`get_market_data`", rules)
if __name__ == "__main__":
unittest.main()
SHA-256: 18b2581835f82ce007d78e538e5359d2ad30fa13f37652bea06aba5da21c6872