1022 lines
43 KiB
Python
1022 lines
43 KiB
Python
"""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 tests.webui_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()
|