#!/usr/bin/env python3 """Backend test for the Workshop's new thesis actions: edit, retire-subtree, choose-variant, and one-click dual-approval -> canonical. Runs init_db (which seeds the v5 thesis) against a throwaway DB, then exercises the architect_tools + thesis_review functions the new routes call. Offline + synthetic. Run: cd backend && python3 test_thesis_actions.py """ import os import sqlite3 import sys import tempfile _tmp = tempfile.mkdtemp() os.environ["CRM_DATA_DIR"] = _tmp os.environ["CRM_DB_PATH"] = os.path.join(_tmp, "crm.db") sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import server # noqa: E402 (the CRM server; it imports architect_tools + thesis_review for us) at = server._architect_tools thesis_review = server.thesis_review FAILS = [] def check(cond, msg): print((" PASS " if cond else " FAIL ") + msg) if not cond: FAILS.append(msg) def main(): server.init_db() db = server.DB_PATH c = sqlite3.connect(db) c.row_factory = sqlite3.Row # choose-variant: pick Option A in the positioning group -> Option B retired opts = c.execute("SELECT id, title FROM thesis_nodes WHERE variant_group='positioning' AND deleted_at IS NULL").fetchall() check(len(opts) == 2, f"positioning starts with 2 options (got {len(opts)})") a = next(o for o in opts if "Option A" in (o["title"] or "")) res = at.choose_variant(a["id"], db=db) check(res.get("retired_siblings") == 1, "choose_variant retired the 1 sibling") live = c.execute("SELECT COUNT(*) FROM thesis_nodes WHERE variant_group='positioning' AND deleted_at IS NULL").fetchone()[0] check(live == 1, f"positioning now has 1 live option (got {live})") check(c.execute("SELECT status FROM thesis_nodes WHERE id=?", (a["id"],)).fetchone()[0] == "approved", "chosen option marked approved") # edit: manual human edit updates body + writes a revision thr = c.execute("SELECT id FROM thesis_nodes WHERE node_type='throughline' AND deleted_at IS NULL").fetchone() at.upsert_thesis_node(None, "throughline", "EDITED THROUGHLINE.", node_id=thr["id"], actor_type="human", change_reason="manual edit", actor_id="u1", db=db) check(c.execute("SELECT body FROM thesis_nodes WHERE id=?", (thr["id"],)).fetchone()[0] == "EDITED THROUGHLINE.", "manual edit updated the node body") rev = c.execute("SELECT actor_type FROM thesis_node_revisions WHERE node_id=? ORDER BY rev_no DESC LIMIT 1", (thr["id"],)).fetchone() check(rev and rev[0] == "human", "edit recorded a 'human' revision") # retire-subtree: retiring the Pillars section drops it + its 3 pillar claims sec = c.execute("SELECT id FROM thesis_nodes WHERE node_type='section' AND title='Pillars' AND deleted_at IS NULL").fetchone() kids = c.execute("SELECT COUNT(*) FROM thesis_nodes WHERE parent_id=? AND deleted_at IS NULL", (sec["id"],)).fetchone()[0] res = at.retire_node(sec["id"], db=db) check(res.get("count") == kids + 1, f"retire_node soft-deleted the section + {kids} children (got {res.get('count')})") check(c.execute("SELECT deleted_at FROM thesis_nodes WHERE id=?", (sec["id"],)).fetchone()[0] is not None, "section is soft-deleted") # one-click dual approval -> canonical (two distinct admins) ver = at.create_thesis_version("core", created_by="u1", db=db) at.submit_version_for_review(ver["id"], db=db) r1 = thesis_review.record_review(db, ver["id"], "u1", "approve") check(r1["approvals"] == 1 and not r1["promoted_to_canonical"], "1st approval does not promote") r2 = thesis_review.record_review(db, ver["id"], "u2", "approve") check(r2["approvals"] == 2 and r2["promoted_to_canonical"], "2nd distinct approval promotes to canonical") check(thesis_review.get_canonical(db, "core").get("status") == "ok", "core line now has a canonical version") # a 2nd approval from the SAME admin would NOT have promoted (distinct approvers only) — covered by record_review c.close() print() if FAILS: print(f"FAILED ({len(FAILS)})") sys.exit(1) print("ALL PASS (thesis workshop actions)") if __name__ == "__main__": main()