test_schema.py 10.3 KB
"""U-001 tests: all 15 tables build, and the constraints named in the Unit's
Done criterion (unique / foreign key / enum) are actually enforced.

Run: python3 -m unittest src.test_schema -v
"""

import sqlite3
import unittest

from src import schema


class SchemaCreationTest(unittest.TestCase):
    def setUp(self):
        self.conn = schema.connect(":memory:")
        schema.create_schema(self.conn)

    def tearDown(self):
        self.conn.close()

    def test_all_15_tables_created(self):
        rows = self.conn.execute(
            "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
        ).fetchall()
        actual = {row[0] for row in rows}
        self.assertEqual(actual, set(schema.TABLE_NAMES))
        self.assertEqual(len(schema.TABLE_NAMES), 15)

    def test_schema_is_idempotent_to_reopen_on_disk(self):
        # not required by the Done criterion, but a schema that only works once
        # against :memory: would be a trap for every later Unit that opens a
        # real db file -- cheap to check now.
        import os
        import tempfile

        fd, path = tempfile.mkstemp(suffix=".sqlite3")
        os.close(fd)
        os.remove(path)
        try:
            conn = schema.connect(path)
            schema.create_schema(conn)
            conn.close()
            conn2 = schema.connect(path)
            rows = conn2.execute(
                "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
            ).fetchall()
            conn2.close()
            self.assertEqual({r[0] for r in rows}, set(schema.TABLE_NAMES))
        finally:
            if os.path.exists(path):
                os.remove(path)


class ConstraintTestBase(unittest.TestCase):
    def setUp(self):
        self.conn = schema.connect(":memory:")
        schema.create_schema(self.conn)

    def tearDown(self):
        self.conn.close()

    def insert_project(self, project_id="proj-1", target_folder_path="/tmp/proj-1"):
        self.conn.execute(
            "INSERT INTO project (project_id, name, target_folder_path, has_git_repo, status) "
            "VALUES (?, ?, ?, 1, 'active')",
            (project_id, "Project One", target_folder_path),
        )
        self.conn.commit()
        return project_id

    def insert_task(self, task_id="task-1", project_id="proj-1", status="queued"):
        self.conn.execute(
            "INSERT INTO task (task_id, project_id, goal_text, task_type, status, assigned_org_node) "
            "VALUES (?, ?, 'implement snake game', 'goal', ?, 'planner')",
            (task_id, project_id, status),
        )
        self.conn.commit()
        return task_id

    def insert_skill(self, skill_id="skill-1"):
        self.conn.execute(
            "INSERT INTO skill (skill_id, owner_org_node, file_path, current_git_commit_sha, source, status) "
            "VALUES (?, 'coder', 'skills/skill-1/', 'abc123', 'new', 'active')",
            (skill_id,),
        )
        self.conn.commit()
        return skill_id

    def insert_work_graph_run(self, run_id="run-1", task_id="task-1", org_node="coder", status="planning"):
        self.conn.execute(
            "INSERT INTO work_graph_run (run_id, task_id, org_node, graph_manifest, status) "
            "VALUES (?, ?, ?, '{}', ?)",
            (run_id, task_id, org_node, status),
        )
        self.conn.commit()
        return run_id


class ProjectUniqueConstraintTest(ConstraintTestBase):
    def test_duplicate_target_folder_path_rejected(self):
        self.insert_project("proj-1", "/tmp/shared-path")
        with self.assertRaises(sqlite3.IntegrityError):
            self.conn.execute(
                "INSERT INTO project (project_id, name, target_folder_path, has_git_repo, status) "
                "VALUES ('proj-2', 'Project Two', '/tmp/shared-path', 1, 'active')"
            )

    def test_has_git_repo_boolean_check_rejects_out_of_range_value(self):
        with self.assertRaises(sqlite3.IntegrityError):
            self.conn.execute(
                "INSERT INTO project (project_id, name, target_folder_path, has_git_repo, status) "
                "VALUES ('proj-1', 'P', '/tmp/p', 2, 'active')"
            )


class WorkNodeExecutionEnumConstraintTest(ConstraintTestBase):
    def setUp(self):
        super().setUp()
        self.insert_project()
        self.insert_task()
        self.insert_skill()
        self.insert_work_graph_run()

    def _insert_node_exec(self, status):
        self.conn.execute(
            "INSERT INTO work_node_execution "
            "(node_exec_id, run_id, node_id, subagent_type, skill_ref, "
            " input_schema_ref, output_schema_ref, input_payload, status) "
            "VALUES ('node-1', 'run-1', 'n1', 'coder', 'skill-1', 'in.json', 'out.json', '{}', ?)",
            (status,),
        )

    def test_valid_status_accepted(self):
        self._insert_node_exec("waiting_for_quota")
        self.conn.commit()
        row = self.conn.execute(
            "SELECT status FROM work_node_execution WHERE node_exec_id = 'node-1'"
        ).fetchone()
        self.assertEqual(row[0], "waiting_for_quota")

    def test_invalid_status_rejected(self):
        with self.assertRaises(sqlite3.IntegrityError):
            self._insert_node_exec("not_a_real_status")


class ForeignKeyEnforcementTest(ConstraintTestBase):
    def test_task_rejects_unknown_project_id(self):
        with self.assertRaises(sqlite3.IntegrityError):
            self.insert_task(task_id="task-1", project_id="does-not-exist")

    def test_work_node_execution_rejects_unknown_skill_ref(self):
        self.insert_project()
        self.insert_task()
        self.insert_work_graph_run()
        with self.assertRaises(sqlite3.IntegrityError):
            self.conn.execute(
                "INSERT INTO work_node_execution "
                "(node_exec_id, run_id, node_id, subagent_type, skill_ref, "
                " input_schema_ref, output_schema_ref, input_payload, status) "
                "VALUES ('node-1', 'run-1', 'n1', 'coder', 'no-such-skill', 'in.json', 'out.json', '{}', 'pending')"
            )


class WorkGraphRunActiveUniqueIndexTest(ConstraintTestBase):
    """01-M 1.3: only one active (planning/running/merging/verifying) run per
    (task_id, org_node) at a time -- terminal-status runs don't count."""

    def setUp(self):
        super().setUp()
        self.insert_project()
        self.insert_task()

    def test_second_active_run_for_same_task_and_org_node_rejected(self):
        self.insert_work_graph_run("run-1", "task-1", "coder", status="running")
        with self.assertRaises(sqlite3.IntegrityError):
            self.insert_work_graph_run("run-2", "task-1", "coder", status="planning")

    def test_second_run_allowed_once_first_is_terminal(self):
        self.insert_work_graph_run("run-1", "task-1", "coder", status="done")
        self.insert_work_graph_run("run-2", "task-1", "coder", status="planning")
        count = self.conn.execute(
            "SELECT COUNT(*) FROM work_graph_run WHERE task_id = 'task-1' AND org_node = 'coder'"
        ).fetchone()[0]
        self.assertEqual(count, 2)


class ArtifactUniqueConstraintTest(ConstraintTestBase):
    def setUp(self):
        super().setUp()
        self.insert_project()
        self.insert_task()
        self.insert_work_graph_run()

    def _insert_artifact(self, artifact_id):
        self.conn.execute(
            "INSERT INTO artifact (artifact_id, artifact_type, task_id, produced_by_run_id, "
            "file_path, git_commit_sha) VALUES (?, 'sdd', 'task-1', 'run-1', 'sdd.md', 'sha-1')",
            (artifact_id,),
        )

    def test_duplicate_task_type_commit_rejected(self):
        self._insert_artifact("artifact-1")
        self.conn.commit()
        with self.assertRaises(sqlite3.IntegrityError):
            self._insert_artifact("artifact-2")


class ExecutionLogFileUniqueConstraintTest(ConstraintTestBase):
    def setUp(self):
        super().setUp()
        self.insert_project()
        self.insert_task()
        self.insert_skill()
        self.insert_work_graph_run()
        self.conn.execute(
            "INSERT INTO work_node_execution "
            "(node_exec_id, run_id, node_id, subagent_type, skill_ref, "
            " input_schema_ref, output_schema_ref, input_payload, status) "
            "VALUES ('node-1', 'run-1', 'n1', 'coder', 'skill-1', 'in.json', 'out.json', '{}', 'running')"
        )
        self.conn.commit()

    def test_second_log_file_for_same_node_exec_rejected(self):
        self.conn.execute(
            "INSERT INTO execution_log_file (log_file_id, node_exec_id, project_id, file_path, format, status) "
            "VALUES ('log-1', 'node-1', 'proj-1', '/logs/node-1.jsonl', 'jsonl', 'active')"
        )
        self.conn.commit()
        with self.assertRaises(sqlite3.IntegrityError):
            self.conn.execute(
                "INSERT INTO execution_log_file (log_file_id, node_exec_id, project_id, file_path, format, status) "
                "VALUES ('log-2', 'node-1', 'proj-1', '/logs/node-1-again.jsonl', 'jsonl', 'active')"
            )


class BugRecordRootCauseCategoryTest(ConstraintTestBase):
    """core_engine is PH5-scoped (U-034); U-001 deliberately does not accept it yet
    (confirmed with user, 2026-07-30) -- this test locks that decision in so a
    future edit can't silently widen the enum back out ahead of U-034."""

    def _insert_bug(self, root_cause_category):
        self.conn.execute(
            "INSERT INTO bug_record (bug_id, source, description, root_cause_category, status) "
            "VALUES ('bug-1', 'validator', 'wall collision misjudged', ?, 'open')",
            (root_cause_category,),
        )

    def test_pre_ph5_categories_accepted(self):
        for category in ("planning", "coder", "skill_defect"):
            with self.subTest(category=category):
                conn = schema.connect(":memory:")
                schema.create_schema(conn)
                conn.execute(
                    "INSERT INTO bug_record (bug_id, source, description, root_cause_category, status) "
                    "VALUES ('bug-1', 'validator', 'x', ?, 'open')",
                    (category,),
                )
                conn.commit()
                conn.close()

    def test_core_engine_rejected_before_u034(self):
        with self.assertRaises(sqlite3.IntegrityError):
            self._insert_bug("core_engine")


if __name__ == "__main__":
    unittest.main()