services: add mood, moodtracker, overleaf, personal-sensing; update ollama
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>
This commit is contained in:
0
mood/tests/__init__.py
Normal file
0
mood/tests/__init__.py
Normal file
112
mood/tests/test_store.py
Normal file
112
mood/tests/test_store.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from src import store
|
||||
|
||||
|
||||
def _fresh_db():
|
||||
path = os.path.join(tempfile.mkdtemp(), "t.sqlite")
|
||||
conn = store.connect(path)
|
||||
store.init_db(conn)
|
||||
return conn
|
||||
|
||||
|
||||
def _row(id_, ts, mood, tags, note="", affirmation=""):
|
||||
return {"id": id_, "ts": ts, "mood": mood, "tags": json.dumps(tags),
|
||||
"note": note, "affirmation": affirmation}
|
||||
|
||||
|
||||
def test_upsert_idempotent():
|
||||
conn = _fresh_db()
|
||||
rows = [
|
||||
_row(1, "2026-07-01T08:00:00+00:00", 4, ["calm"]),
|
||||
_row(2, "2026-07-02T08:00:00+00:00", 2, ["sad", "tired"]),
|
||||
]
|
||||
store.upsert_entries(conn, rows)
|
||||
store.upsert_entries(conn, rows) # re-run same rows
|
||||
count = conn.execute("SELECT COUNT(*) c FROM mood_entries").fetchone()["c"]
|
||||
assert count == 2 # no duplication despite double ingest
|
||||
|
||||
|
||||
def test_upsert_updates_on_conflict():
|
||||
conn = _fresh_db()
|
||||
rows = [_row(1, "2026-07-01T08:00:00+00:00", 4, ["calm"], note="first")]
|
||||
store.upsert_entries(conn, rows)
|
||||
rows[0]["note"] = "corrected"
|
||||
store.upsert_entries(conn, rows)
|
||||
got = conn.execute("SELECT note FROM mood_entries WHERE source_id=1").fetchone()["note"]
|
||||
assert got == "corrected"
|
||||
|
||||
|
||||
def test_sync_state_high_water_mark():
|
||||
conn = _fresh_db()
|
||||
store.set_sync_state(conn, "moodtracker_entries", 5)
|
||||
store.set_sync_state(conn, "moodtracker_entries", 3) # older cursor must not regress
|
||||
assert store.get_last_synced_id(conn, "moodtracker_entries") == 5
|
||||
store.set_sync_state(conn, "moodtracker_entries", 9)
|
||||
assert store.get_last_synced_id(conn, "moodtracker_entries") == 9
|
||||
|
||||
|
||||
def test_summary_and_daily_mood():
|
||||
conn = _fresh_db()
|
||||
rows = [
|
||||
_row(1, "2026-07-01T08:00:00+00:00", 4, ["calm"]),
|
||||
_row(2, "2026-07-01T20:00:00+00:00", 2, ["tired"]),
|
||||
_row(3, "2026-07-02T08:00:00+00:00", 5, ["happy"]),
|
||||
]
|
||||
store.upsert_entries(conn, rows)
|
||||
s = store.summary(conn)
|
||||
assert s["entries"] == 3
|
||||
assert s["coverage"]["earliest"] is not None
|
||||
|
||||
daily = store.daily_mood(conn, days=30)
|
||||
by_day = {d["day"]: d for d in daily}
|
||||
assert by_day["2026-07-01"]["entries"] == 2
|
||||
assert by_day["2026-07-01"]["avg_mood"] == 3.0 # (4+2)/2
|
||||
assert by_day["2026-07-02"]["avg_mood"] == 5.0
|
||||
|
||||
|
||||
def test_tag_breakdown():
|
||||
conn = _fresh_db()
|
||||
rows = [
|
||||
_row(1, "2026-07-01T08:00:00+00:00", 5, ["happy", "energetic"]),
|
||||
_row(2, "2026-07-02T08:00:00+00:00", 1, ["sad", "tired"]),
|
||||
_row(3, "2026-07-03T08:00:00+00:00", 2, ["tired"]),
|
||||
]
|
||||
store.upsert_entries(conn, rows)
|
||||
tags = store.tag_breakdown(conn, days=90, min_count=2)
|
||||
by_tag = {t["tag"]: t for t in tags}
|
||||
assert by_tag["tired"]["count"] == 2
|
||||
assert by_tag["tired"]["avg_mood"] == 1.5
|
||||
assert "happy" not in by_tag # min_count=2 filters singletons
|
||||
|
||||
|
||||
def test_correlate_with_series():
|
||||
conn = _fresh_db()
|
||||
rows = [
|
||||
_row(1, "2026-07-01T08:00:00+00:00", 5, []),
|
||||
_row(2, "2026-07-02T08:00:00+00:00", 4, []),
|
||||
_row(3, "2026-07-03T08:00:00+00:00", 2, []),
|
||||
_row(4, "2026-07-04T08:00:00+00:00", 1, []),
|
||||
]
|
||||
store.upsert_entries(conn, rows)
|
||||
# perfectly correlated external series (e.g. "hours slept")
|
||||
external = {
|
||||
"2026-07-01": 8.0, "2026-07-02": 7.0,
|
||||
"2026-07-03": 5.0, "2026-07-04": 4.0,
|
||||
}
|
||||
result = store.correlate_with_series(conn, external, days=30)
|
||||
assert result["n"] == 4
|
||||
assert result["r"] > 0.99 # near-perfect positive correlation
|
||||
|
||||
|
||||
def test_correlate_too_few_points():
|
||||
conn = _fresh_db()
|
||||
store.upsert_entries(conn, [_row(1, "2026-07-01T08:00:00+00:00", 3, [])])
|
||||
result = store.correlate_with_series(conn, {"2026-07-01": 5.0}, days=30)
|
||||
assert result["n"] == 1
|
||||
assert result["r"] is None
|
||||
108
mood/tests/test_sync.py
Normal file
108
mood/tests/test_sync.py
Normal file
@@ -0,0 +1,108 @@
|
||||
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
|
||||
Reference in New Issue
Block a user