Files
Gitea-Tools/tests/test_webui_timeline.py
T
sysadminandClaude Opus 4.8 8ba1c5b87c fix(webui): make timeline session filter truthful and redact evidence refs (#637)
Remediates the two blocking findings in review #522 on PR #849.

F1 — the session filter dimension was dead end to end. No source could
produce an event carrying a session identifier, so filter_events dropped
every event whenever session was supplied and the API answered with a
green, empty page. An empty result reads to an operator as "no such
session activity", which is a stronger and false claim.

Each source now declares which filter dimensions its records can actually
carry. The control-plane events table is (event_id, work_item_id,
event_type, message, created_at) and records no session, so that source
declares the session dimension unsupported rather than pretending to
answer it; the dead read of a non-existent session_id column is removed.
A CTH handoff comment declares its own Session field, so the handoff
adapter populates session_id from that declared field — authoritative
source data, never inferred from an actor, work item, or message text.

When no source that ran can carry a requested dimension, load_timeline
refuses with ok=false and a structured error naming the unsupported
filters and the per-source reason, and the route answers 422. A source
that can answer the dimension and simply matched nothing still returns
200 with an honest empty page. Ordering, pagination, and the issue/PR
filters are unchanged.

F2 — evidence_refs bypassed redaction and could emit a credential
verbatim. proof and decision were passed to _extract_evidence_refs before
redaction, and the SHA pattern matched any 7-40 character lowercase hex
run, which is exactly the shape of a Gitea access token.

Redaction now runs first and every derived value is taken from the
redacted text. A commit reference is recognised only where the source
text declares one (commit, head, base, sha, ...), so an undeclared hex
run is never lifted out of prose into a structured field; this also drops
the ordinary-word noise the reviewer noted. Every reference is then
independently revalidated against an allowed shape and a second redaction
pass immediately before serialization, failing closed by dropping
anything unproven and flagging the event sensitive. Actor is redacted for
the same reason, and a secret-shaped session value is dropped rather than
emitted. Legitimate issue, PR, short-SHA and full 40-character SHA
references stay usable.

Tests: 16 added. Session filtering is now driven through the CTH adapter
and the composed load_timeline/API path rather than a hand-built
WorkflowEvent, covering a match, an honest empty result, pagination and
ordering under the filter, the 422 refusal, and the per-source support
declaration. Redaction coverage asserts a synthetic 40-character hex
value (not a real credential) appears nowhere in the complete serialized
payload including evidence_refs, that the independent revalidation drops
unproven references, and that legitimate references still resolve.

pytest tests/test_webui_timeline.py: 42 passed (was 26).
pytest -k webui: 340 passed, 270 subtests passed (was 324).
Full suite: 4607 passed, 12 failed, 6 skipped, 684 subtests passed — the
same 12 failures as the master baseline, none under webui/.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-23 16:45:43 -04:00

619 lines
27 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 starlette.testclient import TestClient
import control_plane_db
from canonical_thread_handoff import format_cth_body
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"])
if __name__ == "__main__":
unittest.main()