"""Tests for the workflow-event timeline model and read API (#637). Covers the acceptance criteria: versioned schema, adaptation of control-plane events and Gitea handoff comments, filter by issue/PR/session, redaction of secret-like payloads, and stable pagination. """ import json import os import sqlite3 import sys import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from starlette.testclient import TestClient import control_plane_db from canonical_thread_handoff import ( CTH_TYPES, MARKER, format_cth_body, is_known_cth_type, parse_cth_comment, ) from webui import timeline from webui.app import create_app def _seed_db(path: str) -> None: """Create a control-plane DB and seed scoped work_items + events.""" # Constructing ControlPlaneDB runs the schema migration once. control_plane_db.ControlPlaneDB(db_path=path) conn = sqlite3.connect(path) try: conn.execute( "INSERT INTO work_items(remote, org, repo, kind, number, state, updated_at) " "VALUES (?,?,?,?,?,?,?)", ("prgs", "Scaled-Tech-Consulting", "Gitea-Tools", "issue", 637, "open", "2026-07-23T00:00:00Z"), ) issue_wid = conn.execute("SELECT last_insert_rowid()").fetchone()[0] conn.execute( "INSERT INTO work_items(remote, org, repo, kind, number, state, updated_at) " "VALUES (?,?,?,?,?,?,?)", ("prgs", "Scaled-Tech-Consulting", "Gitea-Tools", "pr", 813, "open", "2026-07-23T00:00:00Z"), ) pr_wid = conn.execute("SELECT last_insert_rowid()").fetchone()[0] # A work item for a different repo — must never appear in prgs/Gitea-Tools scope. conn.execute( "INSERT INTO work_items(remote, org, repo, kind, number, state, updated_at) " "VALUES (?,?,?,?,?,?,?)", ("dadeschools", "Other", "Elsewhere", "issue", 1, "open", "2026-07-23T00:00:00Z"), ) other_wid = conn.execute("SELECT last_insert_rowid()").fetchone()[0] rows = [ (issue_wid, "allocation", "assigned author work", "2026-07-23T01:00:00Z"), (issue_wid, "lease.renew", "token=ghs_ABCDEF1234567890abcdef lease renewed", "2026-07-23T02:00:00Z"), (pr_wid, "pr.opened", "PR opened for review", "2026-07-23T03:00:00Z"), (other_wid, "allocation", "off-scope event", "2026-07-23T04:00:00Z"), ] conn.executemany( "INSERT INTO events(work_item_id, event_type, message, created_at) VALUES (?,?,?,?)", rows, ) conn.commit() finally: conn.close() class TestSchema(unittest.TestCase): def test_schema_is_versioned(self): self.assertIsInstance(timeline.TIMELINE_SCHEMA_VERSION, int) self.assertGreaterEqual(timeline.TIMELINE_SCHEMA_VERSION, 1) def test_event_to_dict_shape(self): ev = timeline.WorkflowEvent( source=timeline.SOURCE_CONTROL_PLANE, event_type="allocation", event_key="cp:1", timestamp="2026-07-23T01:00:00Z", issue_number=637, ) d = ev.to_dict() for key in ( "source", "event_type", "event_key", "timestamp", "actor", "role", "issue_number", "pr_number", "session_id", "tool_name", "decision", "message", "correlation_id", "evidence_refs", "sensitive", ): self.assertIn(key, d) self.assertEqual(d["evidence_refs"], []) class TestCpAdapter(unittest.TestCase): def test_issue_and_pr_mapping(self): rows = [ {"event_id": 1, "event_type": "allocation", "message": "x", "created_at": "2026-07-23T01:00:00Z", "kind": "issue", "number": 637}, {"event_id": 2, "event_type": "pr.opened", "message": "y", "created_at": "2026-07-23T02:00:00Z", "kind": "pr", "number": 813}, ] events = timeline.adapt_cp_events(rows) self.assertEqual(len(events), 2) self.assertEqual(events[0].issue_number, 637) self.assertIsNone(events[0].pr_number) self.assertEqual(events[0].correlation_id, "issue#637") self.assertIsNone(events[1].issue_number) self.assertEqual(events[1].pr_number, 813) def test_malformed_rows_skipped(self): rows = [ {"event_id": None, "event_type": "x", "kind": "issue", "number": 1}, {"event_id": 5, "event_type": "", "kind": "issue", "number": 1}, {"event_id": 6, "event_type": "ok", "message": "m", "created_at": None, "kind": "issue", "number": 1}, ] events = timeline.adapt_cp_events(rows) self.assertEqual(len(events), 1) self.assertIsNone(events[0].timestamp) def test_sensitive_event_flagged(self): rows = [{"event_id": 1, "event_type": "lease.renew", "message": "m", "created_at": "2026-07-23T01:00:00Z", "kind": "issue", "number": 1}] events = timeline.adapt_cp_events(rows) self.assertTrue(events[0].sensitive) class TestCthAdapter(unittest.TestCase): def test_cth_comment_becomes_event(self): body = format_cth_body( cth_type="Author Handoff", status="ready", next_owner="reviewer", decision="implement timeline", proof="commit abc1234 closes #637", next_action="review PR", ready_to_paste_prompt="Review PR #900 as reviewer", ) comments = [{"id": 42, "body": body, "created_at": "2026-07-23T05:00:00Z", "user": {"login": "jcwalker3"}}] events = timeline.adapt_cth_comments(comments, kind="issue", number=637) self.assertEqual(len(events), 1) ev = events[0] self.assertEqual(ev.source, timeline.SOURCE_GITEA_HANDOFF) self.assertEqual(ev.event_type, "handoff:Author Handoff") self.assertEqual(ev.actor, "jcwalker3") self.assertEqual(ev.issue_number, 637) self.assertEqual(ev.event_key, "cth:issue:637:42") self.assertIn("#637", ev.evidence_refs) self.assertIn("abc1234", ev.evidence_refs) def test_non_cth_comment_ignored(self): comments = [{"id": 1, "body": "just a normal comment", "created_at": "2026-07-23T05:00:00Z", "user": {"login": "x"}}] self.assertEqual(timeline.adapt_cth_comments(comments, kind="issue", number=1), []) class TestRedaction(unittest.TestCase): def test_cp_message_redacted(self): rows = [{"event_id": 1, "event_type": "lease", "message": "token=ghs_ABCDEF1234567890abcdef here", "created_at": "2026-07-23T01:00:00Z", "kind": "issue", "number": 1}] events = timeline.adapt_cp_events(rows) self.assertNotIn("ghs_ABCDEF1234567890abcdef", events[0].message or "") def test_handoff_decision_redacted(self): body = format_cth_body( cth_type="Blocker", status="blocked", next_owner="author", decision="password=SuperSecret123! must rotate", proof="none", next_action="rotate", ready_to_paste_prompt="Rotate the credential and retry", ) comments = [{"id": 7, "body": body, "created_at": "2026-07-23T05:00:00Z", "user": {"login": "x"}}] events = timeline.adapt_cth_comments(comments, kind="issue", number=1) self.assertNotIn("SuperSecret123!", events[0].decision or "") class TestFilterSortPaginate(unittest.TestCase): def _events(self): return [ timeline.WorkflowEvent(source="control_plane", event_type="a", event_key="cp:3", timestamp="2026-07-23T03:00:00Z", pr_number=813), timeline.WorkflowEvent(source="control_plane", event_type="b", event_key="cp:1", timestamp="2026-07-23T01:00:00Z", issue_number=637), timeline.WorkflowEvent(source="control_plane", event_type="c", event_key="cp:2", timestamp="2026-07-23T02:00:00Z", issue_number=637, session_id="sess-1"), ] def test_filter_by_issue(self): out = timeline.filter_events(self._events(), issue=637) self.assertEqual({e.event_key for e in out}, {"cp:1", "cp:2"}) def test_filter_by_pr(self): out = timeline.filter_events(self._events(), pr=813) self.assertEqual([e.event_key for e in out], ["cp:3"]) def test_filter_by_session(self): out = timeline.filter_events(self._events(), session="sess-1") self.assertEqual([e.event_key for e in out], ["cp:2"]) def test_stable_sort_ascending(self): out = timeline.sort_events(self._events()) self.assertEqual([e.event_key for e in out], ["cp:1", "cp:2", "cp:3"]) def test_missing_timestamp_sorts_last(self): evs = self._events() + [ timeline.WorkflowEvent(source="control_plane", event_type="z", event_key="cp:9", timestamp=None) ] out = timeline.sort_events(evs) self.assertEqual(out[-1].event_key, "cp:9") def test_pagination_windows_and_next_offset(self): evs = timeline.sort_events(self._events()) page1 = timeline.paginate(evs, limit=2, offset=0) self.assertEqual(len(page1.events), 2) self.assertEqual(page1.total, 3) self.assertEqual(page1.next_offset, 2) page2 = timeline.paginate(evs, limit=2, offset=2) self.assertEqual(len(page2.events), 1) self.assertIsNone(page2.next_offset) def test_pagination_bounds_coerced(self): evs = self._events() page = timeline.paginate(evs, limit=-5, offset=-3) self.assertGreaterEqual(page.limit, 1) self.assertEqual(page.offset, 0) class TestCpReader(unittest.TestCase): def test_reads_scoped_events_only(self): import tempfile with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) events, status = timeline.read_cp_events( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", db_path=db ) self.assertTrue(status.ok) # 3 scoped events; the dadeschools/Other event is excluded. self.assertEqual(len(events), 3) self.assertTrue(all(e.source == "control_plane" for e in events)) # Redaction applied to the token-bearing message. joined = " ".join(e.message or "" for e in events) self.assertNotIn("ghs_ABCDEF1234567890abcdef", joined) def test_missing_db_degrades(self): events, status = timeline.read_cp_events( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", db_path="/nonexistent/path/to/cp.sqlite3", ) self.assertEqual(events, []) self.assertFalse(status.ok) self.assertIsNotNone(status.reason) class TestLoadTimeline(unittest.TestCase): def test_handoff_not_run_without_thread_filter(self): import tempfile with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) snap = timeline.load_timeline( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", db_path=db ) d = snap.to_dict() handoff = [s for s in d["sources"] if s["name"] == "gitea_handoff"][0] self.assertFalse(handoff["ok"]) self.assertIn("thread-scoped", handoff["reason"]) self.assertEqual(d["schema_version"], timeline.TIMELINE_SCHEMA_VERSION) def test_handoff_included_via_injected_source(self): import tempfile body = format_cth_body( cth_type="Author Handoff", status="ready", next_owner="reviewer", decision="d", proof="#637", next_action="review", ready_to_paste_prompt="Review PR #1 now", ) def source(kind, number): return [{"id": 1, "body": body, "created_at": "2026-07-23T09:00:00Z", "user": {"login": "jcwalker3"}}] with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) snap = timeline.load_timeline( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", issue=637, db_path=db, comment_source=source, ) d = snap.to_dict() handoff = [s for s in d["sources"] if s["name"] == "gitea_handoff"][0] self.assertTrue(handoff["ok"]) self.assertEqual(handoff["count"], 1) # Both a CP event and the handoff event for issue 637 appear, sorted. kinds = {e["source"] for e in d["events"]} self.assertEqual(kinds, {"control_plane", "gitea_handoff"}) def test_failing_comment_source_degrades_only_handoff(self): import tempfile def boom(kind, number): raise RuntimeError("network down") with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) snap = timeline.load_timeline( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", issue=637, db_path=db, comment_source=boom, ) d = snap.to_dict() cp = [s for s in d["sources"] if s["name"] == "control_plane"][0] handoff = [s for s in d["sources"] if s["name"] == "gitea_handoff"][0] self.assertTrue(cp["ok"]) self.assertFalse(handoff["ok"]) self.assertIn("network down", handoff["reason"]) # A fabricated 40-character lowercase hex value with the shape of a Gitea # personal access token. Never a real credential — its only job is to prove it # cannot reach any part of a serialized timeline payload. SYNTHETIC_SECRET_40_HEX = "a3f9c17be44d2058e6b17c9d0f5321ab77c4e9d1" def _cth_comment(comment_id, *, created_at, session=None, decision="d", proof="none", **kw): """Build one real CTH comment record, optionally declaring a session.""" extra = {"Session": session} if session is not None else None body = format_cth_body( cth_type=kw.pop("cth_type", "Author Handoff"), status=kw.pop("status", "ready"), next_owner=kw.pop("next_owner", "reviewer"), decision=decision, proof=proof, next_action=kw.pop("next_action", "review"), ready_to_paste_prompt=kw.pop("ready_to_paste_prompt", "Review PR #1 now"), extra_fields=extra, ) return { "id": comment_id, "body": body, "created_at": created_at, "user": {"login": kw.pop("login", "jcwalker3")}, } class TestSessionFilterThroughAdapter(unittest.TestCase): """F1: the session dimension must be real, or explicitly refused. These drive the filter through the CTH adapter and the composed ``load_timeline``/API path, never through a hand-built ``WorkflowEvent``. """ def _source(self, comments): return lambda kind, number: list(comments) def test_adapter_populates_declared_session(self): events = timeline.adapt_cth_comments( [_cth_comment(1, created_at="2026-07-23T05:00:00Z", session="sess-alpha")], kind="issue", number=637, ) self.assertEqual(len(events), 1) self.assertEqual(events[0].session_id, "sess-alpha") def test_session_filter_matches_through_adapter(self): import tempfile comments = [ _cth_comment(1, created_at="2026-07-23T09:00:00Z", session="sess-alpha"), _cth_comment(2, created_at="2026-07-23T08:00:00Z", session="sess-alpha"), _cth_comment(3, created_at="2026-07-23T10:00:00Z", session="sess-beta"), ] with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) snap = timeline.load_timeline( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", issue=637, session="sess-alpha", db_path=db, comment_source=self._source(comments), ) d = snap.to_dict() self.assertTrue(d["ok"]) self.assertIsNone(d["error"]) keys = [e["event_key"] for e in d["events"]] # Only the two sess-alpha events, still in ascending timestamp order. self.assertEqual(keys, ["cth:issue:637:2", "cth:issue:637:1"]) self.assertTrue(all(e["session_id"] == "sess-alpha" for e in d["events"])) self.assertEqual(d["pagination"]["total"], 2) def test_session_filter_paginates_and_keeps_order(self): import tempfile comments = [ _cth_comment(i, created_at=f"2026-07-23T0{i}:00:00Z", session="sess-alpha") for i in range(1, 4) ] with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) kwargs = dict( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", issue=637, session="sess-alpha", db_path=db, comment_source=self._source(comments), ) page1 = timeline.load_timeline(limit=2, offset=0, **kwargs).to_dict() page2 = timeline.load_timeline(limit=2, offset=2, **kwargs).to_dict() self.assertEqual( [e["event_key"] for e in page1["events"]], ["cth:issue:637:1", "cth:issue:637:2"], ) self.assertEqual(page1["pagination"]["next_offset"], 2) self.assertEqual([e["event_key"] for e in page2["events"]], ["cth:issue:637:3"]) self.assertIsNone(page2["pagination"]["next_offset"]) def test_unknown_session_is_honestly_empty_when_supported(self): """A source that *can* answer the dimension may legitimately match nothing.""" import tempfile comments = [_cth_comment(1, created_at="2026-07-23T09:00:00Z", session="sess-alpha")] with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) d = timeline.load_timeline( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", issue=637, session="sess-nope", db_path=db, comment_source=self._source(comments), ).to_dict() self.assertTrue(d["ok"]) self.assertEqual(d["events"], []) def test_session_filter_refused_when_no_source_can_answer(self): """The F1 defect: an empty-and-healthy page for an unanswerable filter.""" import tempfile with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) d = timeline.load_timeline( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", issue=637, session="sess-alpha", db_path=db, comment_source=None, ).to_dict() self.assertFalse(d["ok"]) self.assertEqual(d["error"]["code"], "filter_not_supported") self.assertEqual(d["error"]["unsupported_filters"], ["session"]) self.assertEqual(d["events"], []) self.assertEqual(d["pagination"]["total"], 0) # The refusal states which source could not answer, and why. explained = {r["source"] for r in d["error"]["sources"]} self.assertEqual(explained, {"control_plane", "gitea_handoff"}) def test_control_plane_declares_session_unsupported(self): import tempfile with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) d = timeline.load_timeline( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", issue=637, session="sess-alpha", db_path=db, comment_source=self._source([]), ).to_dict() cp = [s for s in d["sources"] if s["name"] == "control_plane"][0] handoff = [s for s in d["sources"] if s["name"] == "gitea_handoff"][0] self.assertNotIn("session", cp["supported_filters"]) self.assertEqual(cp["unsupported_filters"], ["session"]) self.assertIn("session", handoff["supported_filters"]) self.assertEqual(handoff["unsupported_filters"], []) def test_secret_shaped_session_value_is_dropped(self): events = timeline.adapt_cth_comments( [_cth_comment(1, created_at="2026-07-23T05:00:00Z", session=SYNTHETIC_SECRET_40_HEX)], kind="issue", number=637, ) self.assertIsNone(events[0].session_id) self.assertNotIn(SYNTHETIC_SECRET_40_HEX, json.dumps(events[0].to_dict())) class TestEvidenceRefRedaction(unittest.TestCase): """F2: evidence_refs must not be a hole in the redaction boundary.""" def _refs_for(self, *, proof="none", decision="d"): events = timeline.adapt_cth_comments( [_cth_comment(1, created_at="2026-07-23T05:00:00Z", proof=proof, decision=decision)], kind="issue", number=637, ) self.assertEqual(len(events), 1) return events[0] def test_assigned_secret_never_reaches_evidence_refs(self): ev = self._refs_for(proof=f"authenticated with token={SYNTHETIC_SECRET_40_HEX}") self.assertNotIn(SYNTHETIC_SECRET_40_HEX, ev.evidence_refs) self.assertNotIn(SYNTHETIC_SECRET_40_HEX, json.dumps(ev.to_dict())) def test_bare_secret_shaped_value_never_reaches_evidence_refs(self): # An undeclared hex run in proof text is not evidence of anything, and # proof itself is never serialized — so the value has no way out. ev = self._refs_for(proof=f"proof {SYNTHETIC_SECRET_40_HEX}", decision="rotate the credential") self.assertNotIn(SYNTHETIC_SECRET_40_HEX, ev.evidence_refs) self.assertNotIn(SYNTHETIC_SECRET_40_HEX, json.dumps(ev.to_dict())) def test_secret_absent_from_complete_serialized_payload(self): import tempfile comments = [ _cth_comment( 1, created_at="2026-07-23T09:00:00Z", proof=f"lease token={SYNTHETIC_SECRET_40_HEX} and bare {SYNTHETIC_SECRET_40_HEX}", decision=f"rotate api_key={SYNTHETIC_SECRET_40_HEX}", ) ] with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) snap = timeline.load_timeline( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", issue=637, db_path=db, comment_source=lambda k, n: comments, ) payload = json.dumps(snap.to_dict()) self.assertNotIn(SYNTHETIC_SECRET_40_HEX, payload) # And the surface is clean by the redaction policy's own detectors. from webui import console_redaction self.assertEqual(console_redaction.scan_for_secrets(snap.to_dict()), []) def test_legitimate_references_still_usable(self): head = "4f3a464a1c455b6ecaaae9b6eef496c8f8ed451a" ev = self._refs_for( proof=f"closes #637, PR #849, commit abc1234, at head {head}", decision="none", ) for token in ("#637", "#849", "abc1234", head): self.assertIn(token, ev.evidence_refs) def test_undeclared_hex_words_are_not_references(self): ev = self._refs_for(proof="the record was defaced and the facade decayed") self.assertEqual(ev.evidence_refs, ()) def test_refs_revalidated_independently_before_serialization(self): """Extraction is not trusted: the validator drops anything unproven.""" safe, dropped = timeline._validated_evidence_refs( ["#637", "abc1234", "not-a-ref", f"token={SYNTHETIC_SECRET_40_HEX}", ""] ) self.assertEqual(safe, ("#637", "abc1234")) self.assertTrue(dropped) self.assertNotIn(SYNTHETIC_SECRET_40_HEX, json.dumps(list(safe))) def test_clean_event_is_not_marked_sensitive(self): ev = self._refs_for(proof="closes #637") self.assertEqual(ev.evidence_refs, ("#637",)) self.assertFalse(ev.sensitive) class TestTimelineApi(unittest.TestCase): def setUp(self): self._prev_db = os.environ.get(control_plane_db.DB_PATH_ENV) self._prev_offline = os.environ.get("WEBUI_TEST_OFFLINE") import tempfile self._tmpdir = tempfile.TemporaryDirectory() self._db = os.path.join(self._tmpdir.name, "cp.sqlite3") _seed_db(self._db) os.environ[control_plane_db.DB_PATH_ENV] = self._db os.environ["WEBUI_TEST_OFFLINE"] = "1" self.client = TestClient(create_app()) def tearDown(self): if self._prev_db is None: os.environ.pop(control_plane_db.DB_PATH_ENV, None) else: os.environ[control_plane_db.DB_PATH_ENV] = self._prev_db if self._prev_offline is None: os.environ.pop("WEBUI_TEST_OFFLINE", None) else: os.environ["WEBUI_TEST_OFFLINE"] = self._prev_offline self._tmpdir.cleanup() def test_api_returns_timeline(self): resp = self.client.get("/api/v1/timeline") self.assertEqual(resp.status_code, 200) body = resp.json() self.assertEqual(body["schema_version"], timeline.TIMELINE_SCHEMA_VERSION) self.assertIn("events", body) self.assertIn("pagination", body) self.assertGreaterEqual(body["pagination"]["total"], 1) def test_api_filter_by_issue(self): resp = self.client.get("/api/v1/timeline?issue=637") self.assertEqual(resp.status_code, 200) events = resp.json()["events"] self.assertTrue(events) self.assertTrue(all(e["issue_number"] == 637 for e in events)) def test_api_pagination(self): resp = self.client.get("/api/v1/timeline?limit=1&offset=0") self.assertEqual(resp.status_code, 200) pg = resp.json()["pagination"] self.assertEqual(pg["limit"], 1) self.assertEqual(len(resp.json()["events"]), 1) if pg["total"] > 1: self.assertTrue(pg["has_more"]) def test_api_is_read_only(self): resp = self.client.post("/api/v1/timeline") self.assertIn(resp.status_code, (404, 405)) def test_api_no_secret_leak(self): resp = self.client.get("/api/v1/timeline?issue=637") self.assertNotIn("ghs_ABCDEF1234567890abcdef", resp.text) def test_api_refuses_unanswerable_session_filter(self): """No handoff source is configured offline, so nothing can carry a session.""" resp = self.client.get("/api/v1/timeline?issue=637&session=sess-alpha") self.assertEqual(resp.status_code, 422) body = resp.json() self.assertFalse(body["ok"]) self.assertEqual(body["error"]["code"], "filter_not_supported") self.assertEqual(body["error"]["unsupported_filters"], ["session"]) self.assertEqual(body["events"], []) self.assertEqual(body["pagination"]["total"], 0) def test_api_unfiltered_read_stays_ok(self): body = self.client.get("/api/v1/timeline?issue=637").json() self.assertTrue(body["ok"]) self.assertIsNone(body["error"]) def _seed_event(path: str, *, event_type: str, message: str, created_at: str) -> None: """Append one control-plane event with a caller-chosen ``event_type``. The ``events`` table stores whatever a producer writes, so this seeds the adapter the way a hostile or buggy producer would. """ conn = sqlite3.connect(path) try: wid = conn.execute( "SELECT work_item_id FROM work_items WHERE kind='issue' AND number=637" ).fetchone()[0] conn.execute( "INSERT INTO events(work_item_id, event_type, message, created_at) VALUES (?,?,?,?)", (wid, event_type, message, created_at), ) conn.commit() finally: conn.close() def _raw_cth_body(heading: str, **fields) -> str: """Build a CTH comment with an arbitrary heading. ``format_cth_body`` refuses an undeclared type, which is exactly the write path already covered. The read path must cope with a body that never went through it, so this writes the marker and heading directly. """ lines = [MARKER, f"## CTH: {heading}", ""] base = { "Status": "ready", "Next owner": "reviewer", "Current blocker": "none", "Decision": "d", "Proof": "none", "Next action": "review", "Ready-to-paste prompt": "Review PR #1 now", } base.update(fields) lines.extend(f"{key}: {value}" for key, value in base.items()) return "\n".join(lines) class TestEventTypeBoundary(unittest.TestCase): """F2 residual: event_type must cross the same boundary as every other field. The canary is seeded through ``event_type`` *only*, with a benign message, so message redaction cannot be what makes these pass. Each case inspects ``event_type`` explicitly and then the complete serialized payload. """ # ---- control-plane stored event_type ---------------------------------- # def test_cp_event_type_canary_never_serialized(self): rows = [{ "event_id": 1, "event_type": SYNTHETIC_SECRET_40_HEX, "message": "deploy completed", # benign: no redaction happens here "created_at": "2026-07-23T01:00:00Z", "kind": "issue", "number": 637, }] events = timeline.adapt_cp_events(rows) self.assertEqual(len(events), 1) ev = events[0] # The field itself, inspected directly. self.assertNotIn(SYNTHETIC_SECRET_40_HEX, ev.event_type) self.assertEqual(ev.event_type, timeline.UNSAFE_EVENT_TYPE) # Message redaction is provably not what saved us: it is untouched. self.assertEqual(ev.message, "deploy completed") # And nowhere in the serialized record. self.assertNotIn(SYNTHETIC_SECRET_40_HEX, json.dumps(ev.to_dict())) # An unsafe value is a visible fact, not a silent substitution. self.assertTrue(ev.sensitive) def test_cp_same_canary_in_message_and_event_type(self): """The decisive case: one record, one value, two fields, one verdict.""" rows = [{ "event_id": 2, "event_type": SYNTHETIC_SECRET_40_HEX, "message": f"deploy token={SYNTHETIC_SECRET_40_HEX}", "created_at": "2026-07-23T01:00:00Z", "kind": "issue", "number": 637, }] ev = timeline.adapt_cp_events(rows)[0] self.assertNotIn(SYNTHETIC_SECRET_40_HEX, ev.message or "") self.assertNotIn(SYNTHETIC_SECRET_40_HEX, ev.event_type) self.assertNotIn(SYNTHETIC_SECRET_40_HEX, json.dumps(ev.to_dict())) def test_cp_unsafe_event_type_is_not_rewritten_as_a_valid_one(self): """A refused value must not be disguised as some other real event type.""" rows = [{ "event_id": 3, "event_type": SYNTHETIC_SECRET_40_HEX, "message": "m", "created_at": "2026-07-23T01:00:00Z", "kind": "issue", "number": 637, }] ev = timeline.adapt_cp_events(rows)[0] for legitimate in ("allocation", "pr.opened", "lease.renew", "assigned"): self.assertNotEqual(ev.event_type, legitimate) def test_cp_assigned_secret_in_event_type_refused(self): rows = [{ "event_id": 4, "event_type": f"token={SYNTHETIC_SECRET_40_HEX}", "message": "m", "created_at": "2026-07-23T01:00:00Z", "kind": "issue", "number": 637, }] ev = timeline.adapt_cp_events(rows)[0] self.assertEqual(ev.event_type, timeline.UNSAFE_EVENT_TYPE) self.assertNotIn(SYNTHETIC_SECRET_40_HEX, json.dumps(ev.to_dict())) def test_cp_legitimate_event_types_preserved(self): """Every real producer type in this repo must survive untouched.""" legitimate = [ "allocation", "pr.opened", "lease.renew", "assigned", "lease_released", "lease_expired", "lease_abandoned", "lease_adopted", "dependency_edge_state_change", ] rows = [ { "event_id": i, "event_type": name, "message": "m", "created_at": "2026-07-23T01:00:00Z", "kind": "issue", "number": 637, } for i, name in enumerate(legitimate, start=1) ] events = timeline.adapt_cp_events(rows) self.assertEqual([e.event_type for e in events], legitimate) def test_cp_malformed_event_type_shapes_refused(self): for bad in ("has space", "1leading-digit", "x" * 200, "semi;colon", "new\nline"): with self.subTest(event_type=bad): rows = [{ "event_id": 1, "event_type": bad, "message": "m", "created_at": "2026-07-23T01:00:00Z", "kind": "issue", "number": 637, }] events = timeline.adapt_cp_events(rows) self.assertEqual(events[0].event_type, timeline.UNSAFE_EVENT_TYPE) self.assertNotIn(bad, json.dumps(events[0].to_dict())) # ---- CTH heading event_type ------------------------------------------- # def test_cth_heading_canary_never_serialized(self): comments = [{ "id": 11, "body": _raw_cth_body(SYNTHETIC_SECRET_40_HEX), "created_at": "2026-07-23T05:00:00Z", "user": {"login": "jcwalker3"}, }] events = timeline.adapt_cth_comments(comments, kind="issue", number=637) self.assertEqual(len(events), 1) ev = events[0] self.assertNotIn(SYNTHETIC_SECRET_40_HEX, ev.event_type) self.assertEqual(ev.event_type, timeline.UNKNOWN_HANDOFF_EVENT_TYPE) # No message redaction is doing the work here — the message is benign. self.assertEqual(ev.message, "review") self.assertNotIn(SYNTHETIC_SECRET_40_HEX, json.dumps(ev.to_dict())) self.assertTrue(ev.sensitive) def test_cth_assigned_secret_heading_refused(self): comments = [{ "id": 12, "body": _raw_cth_body(f"token={SYNTHETIC_SECRET_40_HEX}"), "created_at": "2026-07-23T05:00:00Z", "user": {"login": "jcwalker3"}, }] ev = timeline.adapt_cth_comments(comments, kind="issue", number=637)[0] self.assertEqual(ev.event_type, timeline.UNKNOWN_HANDOFF_EVENT_TYPE) self.assertNotIn(SYNTHETIC_SECRET_40_HEX, json.dumps(ev.to_dict())) def test_cth_unknown_and_whitespace_headings_normalized(self): for heading in ("Totally Made Up", "Author Handoff", "author handoff"): with self.subTest(heading=heading): comments = [{ "id": 13, "body": _raw_cth_body(heading), "created_at": "2026-07-23T05:00:00Z", "user": {"login": "x"}, }] events = timeline.adapt_cth_comments(comments, kind="issue", number=637) self.assertEqual(len(events), 1) self.assertEqual(events[0].event_type, timeline.UNKNOWN_HANDOFF_EVENT_TYPE) def test_cth_declared_types_all_preserved(self): """Point 5: no legitimate declared type is lost to the new check.""" for cth_type in sorted(CTH_TYPES): with self.subTest(cth_type=cth_type): comments = [{ "id": 14, "body": _raw_cth_body(cth_type), "created_at": "2026-07-23T05:00:00Z", "user": {"login": "x"}, }] ev = timeline.adapt_cth_comments(comments, kind="issue", number=637)[0] self.assertEqual(ev.event_type, f"handoff:{cth_type}") self.assertFalse(ev.sensitive) def test_cth_surrounding_whitespace_still_matches_contract(self): comments = [{ "id": 15, "body": _raw_cth_body(" Author Handoff "), "created_at": "2026-07-23T05:00:00Z", "user": {"login": "x"}, }] ev = timeline.adapt_cth_comments(comments, kind="issue", number=637)[0] self.assertEqual(ev.event_type, "handoff:Author Handoff") # ---- complete serialized payload, both paths -------------------------- # def _payload_clean(self, snapshot): payload = json.dumps(snapshot.to_dict()) self.assertNotIn(SYNTHETIC_SECRET_40_HEX, payload) from webui import console_redaction self.assertEqual(console_redaction.scan_for_secrets(snapshot.to_dict()), []) return snapshot.to_dict() def test_canary_absent_from_full_payload_via_cp_event_type(self): import tempfile with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) _seed_event( db, event_type=SYNTHETIC_SECRET_40_HEX, message="routine deploy", created_at="2026-07-23T06:00:00Z", ) snap = timeline.load_timeline( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", issue=637, db_path=db, comment_source=lambda k, n: [], ) d = self._payload_clean(snap) types = [e["event_type"] for e in d["events"]] self.assertIn(timeline.UNSAFE_EVENT_TYPE, types) # The legitimate seeded types are still present and unchanged. self.assertIn("allocation", types) def test_canary_absent_from_full_payload_via_cth_heading(self): import tempfile comments = [{ "id": 21, "body": _raw_cth_body(SYNTHETIC_SECRET_40_HEX), "created_at": "2026-07-23T09:00:00Z", "user": {"login": "jcwalker3"}, }] with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) snap = timeline.load_timeline( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", issue=637, db_path=db, comment_source=lambda k, n: comments, ) d = self._payload_clean(snap) self.assertIn( timeline.UNKNOWN_HANDOFF_EVENT_TYPE, [e["event_type"] for e in d["events"]], ) def test_canary_absent_when_seeded_through_both_paths_at_once(self): import tempfile comments = [{ "id": 22, "body": _raw_cth_body(SYNTHETIC_SECRET_40_HEX), "created_at": "2026-07-23T09:00:00Z", "user": {"login": "jcwalker3"}, }] with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) _seed_event( db, event_type=SYNTHETIC_SECRET_40_HEX, message=f"deploy token={SYNTHETIC_SECRET_40_HEX}", created_at="2026-07-23T06:00:00Z", ) snap = timeline.load_timeline( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", issue=637, db_path=db, comment_source=lambda k, n: comments, ) self._payload_clean(snap) class TestSerializedFieldAudit(unittest.TestCase): """The remaining externally influenced strings that reach the payload.""" def test_event_key_ids_must_be_plain_identifiers(self): rows = [ {"event_id": SYNTHETIC_SECRET_40_HEX, "event_type": "allocation", "message": "m", "created_at": "2026-07-23T01:00:00Z", "kind": "issue", "number": 637}, {"event_id": 8, "event_type": "allocation", "message": "m", "created_at": "2026-07-23T01:00:00Z", "kind": "issue", "number": 637}, ] events = timeline.adapt_cp_events(rows) self.assertEqual([e.event_key for e in events], ["cp:8"]) def test_comment_id_must_be_a_plain_identifier(self): comments = [{ "id": SYNTHETIC_SECRET_40_HEX, "body": _raw_cth_body("Author Handoff"), "created_at": "2026-07-23T05:00:00Z", "user": {"login": "x"}, }] self.assertEqual(timeline.adapt_cth_comments(comments, kind="issue", number=637), []) def test_adapter_refuses_a_scope_it_cannot_express(self): comments = [{ "id": 1, "body": _raw_cth_body("Author Handoff"), "created_at": "2026-07-23T05:00:00Z", "user": {"login": "x"}, }] self.assertEqual( timeline.adapt_cth_comments(comments, kind="issue", number=SYNTHETIC_SECRET_40_HEX), [], ) self.assertEqual( timeline.adapt_cth_comments(comments, kind=SYNTHETIC_SECRET_40_HEX, number=1), [], ) def test_source_failure_reason_is_redacted(self): def boom(kind, number): raise RuntimeError(f"auth failed with token={SYNTHETIC_SECRET_40_HEX}") import tempfile with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) snap = timeline.load_timeline( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", issue=637, db_path=db, comment_source=boom, ) d = snap.to_dict() handoff = [s for s in d["sources"] if s["name"] == "gitea_handoff"][0] self.assertFalse(handoff["ok"]) self.assertIn("handoff source failed", handoff["reason"]) self.assertNotIn(SYNTHETIC_SECRET_40_HEX, json.dumps(d)) def test_echoed_scope_and_filters_are_guarded(self): import tempfile with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "cp.sqlite3") _seed_db(db) d = timeline.load_timeline( remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", issue=637, session=SYNTHETIC_SECRET_40_HEX, db_path=db, comment_source=lambda k, n: [], ).to_dict() self.assertNotIn(SYNTHETIC_SECRET_40_HEX, json.dumps(d)) # Ordinary scope values are untouched, so the echo stays useful. self.assertEqual( d["scope"], {"remote": "prgs", "org": "Scaled-Tech-Consulting", "repo": "Gitea-Tools"}, ) self.assertEqual(d["filters"]["issue"], 637) class TestCthTypeContract(unittest.TestCase): """The contract has one authority; the read path consults it.""" def test_declared_types_are_known(self): for cth_type in CTH_TYPES: self.assertTrue(is_known_cth_type(cth_type)) def test_undeclared_types_are_not_known(self): for value in ("", None, "Made Up", SYNTHETIC_SECRET_40_HEX, "author handoff"): self.assertFalse(is_known_cth_type(value)) def test_parse_reports_contract_membership(self): known = parse_cth_comment(_raw_cth_body("Author Handoff")) self.assertTrue(known["cth_type_known"]) self.assertEqual(known["cth_type"], "Author Handoff") unknown = parse_cth_comment(_raw_cth_body("Not A Real Type")) # Parsing stays total: the type is still reported, just not endorsed. self.assertEqual(unknown["cth_type"], "Not A Real Type") self.assertFalse(unknown["cth_type_known"]) if __name__ == "__main__": unittest.main()