From b0677baf3058b8bde2c00d6d000d4e80c1fa2153 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 22 Mar 2026 05:40:43 +0000 Subject: [PATCH 1/2] fix(surveys): stable order for unprocessed answers when timestamps tie SQLite orders rows with identical created_at arbitrarily. Tie-break on id so fetch_unprocessed_* results are deterministic under concurrency or rapid inserts (avoids flaky tests and consumers assuming FIFO order). Co-authored-by: nic --- python/surveys/db.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/surveys/db.py b/python/surveys/db.py index 6bc5e4012..3c54a9db0 100644 --- a/python/surveys/db.py +++ b/python/surveys/db.py @@ -226,7 +226,7 @@ class SurveyDB: SELECT id, session_id, question_text, field_kind, selector, answer_text, field_json, raw_json, created_at FROM survey_answers WHERE processed=0 - ORDER BY created_at ASC + ORDER BY created_at ASC, id ASC LIMIT ? """, (limit,), @@ -259,7 +259,7 @@ class SurveyDB: FROM survey_answers a JOIN survey_sessions s ON s.id = a.session_id WHERE a.processed=0 - ORDER BY a.created_at ASC + ORDER BY a.created_at ASC, a.id ASC LIMIT ? """, (limit,), From eaefc4f2ec060437d2259b2c4c5bc073e9eb4bbf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 22 Mar 2026 06:44:24 +0000 Subject: [PATCH 2/2] test(surveys): assert stable order when created_at timestamps tie Patch _now_ts so two answers share the same second; verify both fetch paths return rows ordered by id when timestamps match. Co-authored-by: nic --- python/surveys/tests/test_parser_db.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/python/surveys/tests/test_parser_db.py b/python/surveys/tests/test_parser_db.py index c59dff432..db0385047 100644 --- a/python/surveys/tests/test_parser_db.py +++ b/python/surveys/tests/test_parser_db.py @@ -1,4 +1,5 @@ import unittest +from unittest.mock import patch from python.surveys.db import SurveyDB from python.surveys.parser import parse_survey_page @@ -55,6 +56,29 @@ class TestSurveyDB(unittest.TestCase): finally: db.close() + def test_unprocessed_order_stable_when_created_at_ties(self): + """Same-second inserts must not rely on undefined SQLite ordering.""" + fixed_ts = 1_700_000_000 + with patch("python.surveys.db._now_ts", return_value=fixed_ts): + db = SurveyDB(":memory:") + try: + persona = Persona(id="p1", name="Test", description="x", constraints={}) + db.upsert_persona(persona) + profile = UserProfile(id="default", persona_id="p1", data={}) + db.upsert_profile(profile) + db.create_session("s1", url="file://demo", persona_id="p1", profile_id="default") + field = SurveyField(selector="1a", kind=FieldKind.TEXT, label="Q") + # Insert lexicographically later id first; tie-break must still order by id ASC. + db.insert_answer("b2", "s1", "Q", field, "second") + db.insert_answer("a1", "s1", "Q", field, "first") + + ids_plain = [r["id"] for r in db.fetch_unprocessed_answers()] + ids_events = [r["id"] for r in db.fetch_unprocessed_answer_events()] + self.assertEqual(ids_plain, ["a1", "b2"]) + self.assertEqual(ids_events, ["a1", "b2"]) + finally: + db.close() + class TestDeepMerge(unittest.TestCase): def test_deep_merge(self):