| """Tests for the two-database data layer: seed integrity, the SELECT guard, and |
| cross-DB reads through the attached ``inv`` / ``txn`` surface. |
| |
| The ``run_select`` / ``is_safe_select`` guard is the only path LLM-generated SQL |
| takes, so its allow/deny behaviour is security-critical and tested hard. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import pytest |
|
|
| from dukaan import db |
|
|
|
|
| |
| def test_seed_counts(seeded_db): |
| """The research-generated seed must land a rich, realistic dataset.""" |
| c = db.counts() |
| assert c["items"] >= 100 |
| assert c["suppliers"] >= 8 |
| assert c["customers"] >= 20 |
| assert c["sales"] >= 500 |
| assert c["ledger"] >= 10 |
|
|
|
|
| |
| @pytest.mark.parametrize("sql", ["SELECT 1", "WITH x AS (SELECT 1) SELECT * FROM x"]) |
| def test_is_safe_select_allows_reads(sql): |
| ok, _ = db.is_safe_select(sql) |
| assert ok is True |
|
|
|
|
| @pytest.mark.parametrize( |
| "sql", |
| [ |
| "INSERT INTO sales(qty) VALUES (1)", |
| "UPDATE inventory SET qty = 0", |
| "DELETE FROM sales", |
| "DROP TABLE sales", |
| "ATTACH DATABASE 'x.db' AS y", |
| "PRAGMA table_info(sales)", |
| "SELECT 1; DROP TABLE sales", |
| ], |
| ) |
| def test_is_safe_select_blocks_writes(sql): |
| ok, reason = db.is_safe_select(sql) |
| assert ok is False |
| assert isinstance(reason, str) and reason |
|
|
|
|
| |
| def test_run_select_inventory(seeded_db): |
| res = db.run_select("SELECT name, qty FROM inv.inventory ORDER BY name LIMIT 10") |
| assert res["ok"] is True and res["error"] is None |
| assert "name" in res["columns"] |
| assert len(res["rows"]) == 10 |
|
|
|
|
| def test_run_select_cross_db_join(seeded_db): |
| """The agent's key capability: JOIN inv.* with txn.* in one read query.""" |
| res = db.run_select( |
| "SELECT i.name, SUM(s.qty) sold FROM txn.sales s " |
| "JOIN inv.inventory i ON i.item_id = s.item_id GROUP BY i.item_id " |
| "ORDER BY sold DESC LIMIT 5" |
| ) |
| assert res["ok"] is True |
| assert len(res["rows"]) >= 1 |
| assert "name" in res["columns"] and "sold" in res["columns"] |
|
|
|
|
| def test_run_select_rejects_delete(seeded_db): |
| before = db.qx("SELECT COUNT(*) AS n FROM txn.sales")[0]["n"] |
| res = db.run_select("DELETE FROM sales") |
| assert res["ok"] is False and res["rows"] == [] |
| after = db.qx("SELECT COUNT(*) AS n FROM txn.sales")[0]["n"] |
| assert after == before |
|
|