Compose and supporting code for four services that had been running or prototyped without their config tracked here, per the repo convention that agap_git holds the compose + config while application source lives in each service's own Gitea repo. Only placeholder credentials are included: mood/.env.example and moodtracker/.env.example ship dummy values, and overleaf/variables.env carries app name and feature flags only. Real values stay in Vaultwarden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
109 lines
3.5 KiB
Python
109 lines
3.5 KiB
Python
import json
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
import tempfile
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from src import store
|
|
from src.sync import run_sync
|
|
|
|
|
|
def _mock_moodtracker_db(entries):
|
|
"""Build a SQLite file with moodtracker's exact `entries` schema
|
|
(see /home/alvis/moodtracker/app.py init_db) and seed rows."""
|
|
path = os.path.join(tempfile.mkdtemp(), "mood.db")
|
|
conn = sqlite3.connect(path)
|
|
conn.execute("""
|
|
CREATE TABLE entries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ts TEXT NOT NULL,
|
|
mood INTEGER NOT NULL,
|
|
tags TEXT NOT NULL,
|
|
note TEXT,
|
|
affirmation TEXT
|
|
)
|
|
""")
|
|
for ts, mood, tags, note, aff in entries:
|
|
conn.execute(
|
|
"INSERT INTO entries (ts, mood, tags, note, affirmation) VALUES (?,?,?,?,?)",
|
|
(ts, mood, json.dumps(tags), note, aff),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
return path
|
|
|
|
|
|
def _archive_conn():
|
|
path = os.path.join(tempfile.mkdtemp(), "archive.sqlite")
|
|
conn = store.connect(path)
|
|
store.init_db(conn)
|
|
return conn
|
|
|
|
|
|
def test_sync_pulls_all_rows_first_run():
|
|
source = _mock_moodtracker_db([
|
|
("2026-07-01T08:00:00+00:00", 4, ["calm"], "n1", "a1"),
|
|
("2026-07-02T08:00:00+00:00", 2, ["sad", "tired"], "n2", ""),
|
|
])
|
|
conn = _archive_conn()
|
|
totals = run_sync(conn, source_db_path=source)
|
|
assert totals["errors"] == []
|
|
assert totals["entries"] == 2
|
|
assert conn.execute("SELECT COUNT(*) c FROM mood_entries").fetchone()["c"] == 2
|
|
|
|
|
|
def test_sync_is_idempotent_across_runs():
|
|
source = _mock_moodtracker_db([
|
|
("2026-07-01T08:00:00+00:00", 4, ["calm"], "", ""),
|
|
])
|
|
conn = _archive_conn()
|
|
run_sync(conn, source_db_path=source)
|
|
run_sync(conn, source_db_path=source) # nothing new, cursor unchanged
|
|
run_sync(conn, source_db_path=source)
|
|
assert conn.execute("SELECT COUNT(*) c FROM mood_entries").fetchone()["c"] == 1
|
|
|
|
|
|
def test_sync_picks_up_new_rows_incrementally():
|
|
path = _mock_moodtracker_db([
|
|
("2026-07-01T08:00:00+00:00", 4, ["calm"], "", ""),
|
|
])
|
|
conn = _archive_conn()
|
|
run_sync(conn, source_db_path=path)
|
|
assert conn.execute("SELECT COUNT(*) c FROM mood_entries").fetchone()["c"] == 1
|
|
|
|
# a new entry gets logged upstream between syncs
|
|
src_conn = sqlite3.connect(path)
|
|
src_conn.execute(
|
|
"INSERT INTO entries (ts, mood, tags, note, affirmation) VALUES (?,?,?,?,?)",
|
|
("2026-07-03T09:00:00+00:00", 5, json.dumps(["happy"]), "", ""),
|
|
)
|
|
src_conn.commit()
|
|
src_conn.close()
|
|
|
|
run_sync(conn, source_db_path=path)
|
|
assert conn.execute("SELECT COUNT(*) c FROM mood_entries").fetchone()["c"] == 2
|
|
|
|
|
|
def test_sync_records_error_when_source_missing():
|
|
conn = _archive_conn()
|
|
totals = run_sync(conn, source_db_path="/nonexistent/path/mood.db")
|
|
assert totals["errors"]
|
|
row = conn.execute(
|
|
"SELECT last_status, last_error FROM sync_state WHERE stream_key='moodtracker_entries'"
|
|
).fetchone()
|
|
assert row["last_status"] == "error"
|
|
assert row["last_error"]
|
|
|
|
|
|
def test_sync_never_writes_to_source_db():
|
|
"""The source connection is opened read-only; a failed write attempt
|
|
would raise, and run_sync should never attempt one in the first place."""
|
|
source = _mock_moodtracker_db([("2026-07-01T08:00:00+00:00", 3, [], "", "")])
|
|
before = os.path.getmtime(source)
|
|
conn = _archive_conn()
|
|
run_sync(conn, source_db_path=source)
|
|
after = os.path.getmtime(source)
|
|
assert before == after
|