"""Behavioral fixtures for the live-chat selection boundary checker."""
from __future__ import annotations
import importlib.util
import unittest
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
SPEC = importlib.util.spec_from_file_location(
"check_chat_selection_boundary",
SCRIPT_DIR / "check_chat_selection_boundary.py",
)
assert SPEC and SPEC.loader
CHECKER = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(CHECKER)
def clean_sources() -> dict[str, str]:
sources = {
CHECKER.LIVE_TRANSCRIPT_FILES[0]: (
"struct ChatBubble { let body = OmiMarkdown(text: text, sender: .ai) }\n"
),
CHECKER.LIVE_TRANSCRIPT_FILES[1]: (
"struct ChatMessagesView { let body = LazyVStack { ChatBubble() } }\n"
),
CHECKER.MARKDOWN_FILE: (
"struct OmiMarkdown { var body: some View { Text(text).textSelection(.disabled) } }\n"
),
CHECKER.SELECTION_FILE: (
"struct ChatSelectableProseText: NSViewRepresentable {\n"
" func makeNSView(context: Context) -> NSTextView { ChatProseTextView() }\n"
"}\n"
),
}
sources.update(conversation_detail_sources())
return sources
def conversation_detail_sources() -> dict[str, str]:
return {
CHECKER.CONVERSATION_DETAIL_FILES[0]: (
"struct ConversationDetailView {\n"
" var summary: some View {\n"
" OmiMarkdown(text: selection.content, sender: .ai, appKitProseSelection: true)\n"
" .frame(maxWidth: .infinity, alignment: .leading)\n"
" }\n"
" var actionItem: some View {\n"
" Text(item.description)\n"
" .scaledFont(size: OmiType.body)\n"
" .strikethrough(item.completed)\n"
" }\n"
"}\n"
),
CHECKER.CONVERSATION_DETAIL_FILES[1]: (
"struct ConversationSummarySections {\n"
" var heading: some View {\n"
" Text(section.heading)\n"
" .scaledFont(size: OmiType.body, weight: .semibold)\n"
" }\n"
" var body: some View {\n"
" OmiMarkdown(text: section.bodyMarkdown, sender: .ai, appKitProseSelection: true)\n"
" }\n"
"}\n"
),
CHECKER.CONVERSATION_DETAIL_FILES[2]: (
"struct ConversationDetailHeader {\n"
" var title: some View {\n"
" Text(conversation.title)\n"
" .scaledFont(size: OmiType.heading, weight: .semibold)\n"
" }\n"
"}\n"
),
CHECKER.CONVERSATION_DETAIL_FILES[3]: (
"struct ConversationDetailSections {\n"
" var overview: some View {\n"
" OmiMarkdown(text: content, sender: .ai, appKitProseSelection: true, documentProse: true)\n"
" }\n"
"}\n"
),
}
class ChatSelectionBoundaryTests(unittest.TestCase):
def test_accepts_copy_only_live_transcript(self) -> None:
self.assertEqual(CHECKER.check_sources(clean_sources()), [])
def test_rejects_enabled_selection_in_every_protected_surface(self) -> None:
for relative in CHECKER.LIVE_TRANSCRIPT_FILES:
with self.subTest(relative=relative):
sources = clean_sources()
sources[relative] += "let selectable = Text(\"reply\").textSelection(.enabled)\n"
failures = CHECKER.check_sources(sources)
self.assertTrue(any(relative in failure and "SelectionOverlay" in failure for failure in failures))
def test_rejects_selection_escape_hatch(self) -> None:
sources = clean_sources()
sources[CHECKER.MARKDOWN_FILE] += "let textSelectionEnabled = true\n"
failures = CHECKER.check_sources(sources)
self.assertTrue(any("escape hatch" in failure for failure in failures))
def test_requires_explicit_disabled_boundary(self) -> None:
sources = clean_sources()
sources[CHECKER.MARKDOWN_FILE] = "struct OmiMarkdown { var body: some View { Text(text) } }\n"
failures = CHECKER.check_sources(sources)
self.assertTrue(any("explicitly disable" in failure for failure in failures))
def test_requires_the_selection_surface_to_stay_appkit(self) -> None:
"""The remedy is an NSTextView owning its own selection. A SwiftUI
rewrite of this file would put SelectionOverlay back in the transcript
under a name the pattern check cannot see."""
sources = clean_sources()
sources[CHECKER.SELECTION_FILE] = (
"struct ChatSelectableProseText: View { var body: some View { Text(text) } }\n"
)
failures = CHECKER.check_sources(sources)
self.assertTrue(any("NSTextView" in failure for failure in failures))
def test_rejects_missing_protected_source(self) -> None:
sources = clean_sources()
missing = CHECKER.LIVE_TRANSCRIPT_FILES[1]
del sources[missing]
failures = CHECKER.check_sources(sources)
self.assertIn(f"{missing}: protected live-transcript source is missing", failures)
def test_accepts_appkit_prose_in_detail_without_swiftui_selection(self) -> None:
sources = clean_sources()
sources.update(conversation_detail_sources())
self.assertEqual(CHECKER.check_sources(sources), [])
def test_rejects_any_swiftui_selection_in_conversation_detail_files(self) -> None:
"""The detail files take the transcript's blanket ban: even short
`Text` rows there (headings, action items) must not carry SwiftUI
selection — copyable prose in this surface is AppKit-hosted."""
snippets = {
CHECKER.CONVERSATION_DETAIL_FILES[0]: (
" var actionItem: some View {\n"
" Text(item.description).textSelection(.enabled)\n"
" }\n"
),
CHECKER.CONVERSATION_DETAIL_FILES[1]: (
" var heading: some View {\n"
" Text(section.heading)\n"
" .textSelection(.enabled)\n"
" }\n"
),
}
for relative, snippet in snippets.items():
with self.subTest(relative=relative):
sources = clean_sources()
sources.update(conversation_detail_sources())
sources[relative] += snippet
failures = CHECKER.check_sources(sources)
self.assertTrue(
any(
relative in failure and "conversation detail joins" in failure
for failure in failures
)
)
def test_rejects_swiftui_selection_chained_onto_detail_markdown(self) -> None:
shapes = {
"same-line": (
'OmiMarkdown(text: x, sender: .ai).textSelection(.enabled)\n'
),
"next-line": (
"OmiMarkdown(text: selection.content, sender: .ai)\n"
" .textSelection(.enabled)\n"
),
"through-another-modifier": (
"OmiMarkdown(text: x, sender: .ai)\n"
" .padding(4)\n"
" .textSelection(.enabled)\n"
),
"nested-parens-and-escape": (
'OmiMarkdown(text: String(result.content.prefix(200)) + "\\u{2026}", sender: .ai)\n'
" .textSelection(.enabled)\n"
),
}
for relative in CHECKER.CONVERSATION_DETAIL_FILES:
for name, snippet in shapes.items():
with self.subTest(relative=relative, shape=name):
sources = clean_sources()
sources.update(conversation_detail_sources())
sources[relative] += snippet
failures = CHECKER.check_sources(sources)
self.assertTrue(
any(
relative in failure and "conversation detail joins" in failure
for failure in failures
)
)
def test_rejects_chained_omimarkdown_selection_on_any_desktop_surface(self) -> None:
"""The rule is repo-wide, not a fixed file list: the next surface that
reinstalls the banned ancestor is a file this checker has never heard
of. This is what would have caught conversation detail on the PR that
introduced it."""
shapes = {
"same-line": 'OmiMarkdown(text: x, sender: .ai).textSelection(.enabled)\n',
"next-line": (
"OmiMarkdown(text: insight, sender: .ai)\n"
" .frame(maxWidth: .infinity)\n"
" .textSelection(.enabled)\n"
),
}
for name, snippet in shapes.items():
with self.subTest(shape=name):
sources = clean_sources()
sources.update(conversation_detail_sources())
sources["desktop/macos/Desktop/Sources/Surfaces/InsightPanelView.swift"] = (
"struct InsightPanelView: View {\n" + snippet + "}\n"
)
failures = CHECKER.check_sources(sources)
self.assertTrue(
any(
"InsightPanelView.swift" in failure
and "must not gain a SwiftUI-selection" in failure
for failure in failures
)
)
def test_short_text_selection_elsewhere_is_not_the_chained_rule(self) -> None:
"""Plain `Text(...).textSelection(.enabled)` on other surfaces — task
panels, referrals, and the live transcript's per-bubble rows (a
deliberate sibling, fixed separately) — is not this failure class: a
single-line `Text` has a stable intrinsic size and installs no overlay
around tall markdown."""
sources = clean_sources()
sources.update(conversation_detail_sources())
sources["desktop/macos/Desktop/Sources/MainWindow/Components/LiveTranscriptView.swift"] = (
"struct LiveTranscriptBubble: View {\n"
" var body: some View {\n"
" Text(segment.text)\n"
" .scaledFont(size: OmiType.body)\n"
" .textSelection(.enabled)\n"
" .padding(.horizontal, OmiSpacing.md)\n"
" }\n"
"}\n"
)
sources["desktop/macos/Desktop/Sources/MainWindow/Tasks/TaskDetailPanel.swift"] = (
"struct TaskRow: View {\n"
" var body: some View {\n"
" Text(value).textSelection(.enabled)\n"
" }\n"
"}\n"
)
self.assertEqual(CHECKER.check_sources(sources), [])
if __name__ == "__main__":
unittest.main()