mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 11:43:26 +08:00
292 lines
10 KiB
Python
292 lines
10 KiB
Python
"""Tests for ``render_dc`` pure-function helpers.
|
|
|
|
The renderer is large (863 stmts); covering its end-to-end markdown
|
|
output requires a complex tree fixture. These tests target the small,
|
|
pure helpers — they're cheap to test and account for ~150 stmts of the
|
|
gap.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from datetime import datetime, timezone
|
|
|
|
from . import _bootstrap # noqa: F401 — sys.path setup
|
|
|
|
import render_dc # type: ignore
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _parse_iso
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class ParseIsoTests(unittest.TestCase):
|
|
|
|
def test_returns_none_for_falsy_input(self):
|
|
self.assertIsNone(render_dc._parse_iso(None))
|
|
self.assertIsNone(render_dc._parse_iso(""))
|
|
|
|
def test_parses_z_terminated(self):
|
|
self.assertEqual(
|
|
render_dc._parse_iso("2026-04-22T10:00:00Z"),
|
|
datetime(2026, 4, 22, 10, 0, 0, tzinfo=timezone.utc),
|
|
)
|
|
|
|
def test_returns_none_for_unparseable(self):
|
|
self.assertIsNone(render_dc._parse_iso("not-a-timestamp"))
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _fmt_offset
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class FmtOffsetTests(unittest.TestCase):
|
|
|
|
def test_returns_dash_when_timestamp_or_start_missing(self):
|
|
anchor = datetime(2026, 4, 22, 10, 0, 0, tzinfo=timezone.utc)
|
|
self.assertEqual(render_dc._fmt_offset(None, anchor), "—")
|
|
self.assertEqual(render_dc._fmt_offset("2026-04-22T10:00:00Z", None), "—")
|
|
|
|
def test_formats_offset_with_3_decimals(self):
|
|
anchor = datetime(2026, 4, 22, 10, 0, 0, tzinfo=timezone.utc)
|
|
out = render_dc._fmt_offset("2026-04-22T10:00:01.234Z", anchor)
|
|
self.assertEqual(out, "+1.234s")
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _fmt_duration_ms
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class FmtDurationMsTests(unittest.TestCase):
|
|
|
|
def test_dash_when_either_side_missing(self):
|
|
self.assertEqual(render_dc._fmt_duration_ms(None, "2026-04-22T10:00:00Z"), "—")
|
|
self.assertEqual(render_dc._fmt_duration_ms("2026-04-22T10:00:00Z", None), "—")
|
|
|
|
def test_returns_int_ms_with_unit(self):
|
|
out = render_dc._fmt_duration_ms(
|
|
"2026-04-22T10:00:00.000Z", "2026-04-22T10:00:00.123Z",
|
|
)
|
|
self.assertEqual(out, "123ms")
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _decode + _truncate
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class DecodeTruncateTests(unittest.TestCase):
|
|
|
|
def test_decode_unescapes_html_and_collapses_newlines(self):
|
|
self.assertEqual(render_dc._decode(""hi"\nthere"), '"hi" there')
|
|
|
|
def test_decode_returns_empty_for_falsy(self):
|
|
self.assertEqual(render_dc._decode(None), "")
|
|
self.assertEqual(render_dc._decode(""), "")
|
|
|
|
def test_truncate_pads_with_ellipsis_when_over_n(self):
|
|
long = "a" * 100
|
|
out = render_dc._truncate(long, n=10)
|
|
self.assertEqual(len(out), 10)
|
|
self.assertTrue(out.endswith("…"))
|
|
|
|
def test_truncate_returns_input_when_under_or_equal_n(self):
|
|
self.assertEqual(render_dc._truncate("short", n=10), "short")
|
|
|
|
def test_truncate_returns_dash_for_falsy(self):
|
|
self.assertEqual(render_dc._truncate(None), "—")
|
|
self.assertEqual(render_dc._truncate(""), "—")
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _short
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class ShortTests(unittest.TestCase):
|
|
|
|
def test_returns_dash_for_falsy(self):
|
|
self.assertEqual(render_dc._short(None), "—")
|
|
self.assertEqual(render_dc._short(""), "—")
|
|
|
|
def test_truncates_long_uuid_with_ellipsis(self):
|
|
out = render_dc._short("019dface-0000-7000-8000-000000000001", keep=8)
|
|
self.assertEqual(out, "019dface…")
|
|
|
|
def test_returns_input_when_short(self):
|
|
self.assertEqual(render_dc._short("abc"), "abc")
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _fmt_total_duration — h / m / s rollover
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class FmtTotalDurationTests(unittest.TestCase):
|
|
|
|
def test_returns_none_when_either_side_missing(self):
|
|
self.assertIsNone(render_dc._fmt_total_duration(None, "2026-04-22T10:00:00Z"))
|
|
|
|
def test_seconds_only(self):
|
|
self.assertEqual(
|
|
render_dc._fmt_total_duration(
|
|
"2026-04-22T10:00:00.000Z", "2026-04-22T10:00:42.500Z",
|
|
),
|
|
"42.500s",
|
|
)
|
|
|
|
def test_minutes_and_seconds(self):
|
|
self.assertEqual(
|
|
render_dc._fmt_total_duration(
|
|
"2026-04-22T10:00:00Z", "2026-04-22T10:02:30Z",
|
|
),
|
|
"2m 30.000s",
|
|
)
|
|
|
|
def test_hours_minutes_seconds(self):
|
|
out = render_dc._fmt_total_duration(
|
|
"2026-04-22T10:00:00Z", "2026-04-22T11:30:45Z",
|
|
)
|
|
self.assertEqual(out, "1h 30m 45.000s")
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _derive_session_end
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class DeriveSessionEndTests(unittest.TestCase):
|
|
|
|
def test_returns_session_end_ts_verbatim_when_present(self):
|
|
end, source = render_dc._derive_session_end(
|
|
{"end_ts": "2026-04-22T11:00:00Z", "interactions": []},
|
|
)
|
|
self.assertEqual(end, "2026-04-22T11:00:00Z")
|
|
self.assertIsNone(source)
|
|
|
|
def test_uses_session_end_interaction_start_ts(self):
|
|
sess = {
|
|
"end_ts": None,
|
|
"interactions": [
|
|
{"type": "TURN", "start_ts": "10:00", "end_ts": "10:01"},
|
|
{"type": "SESSION_END", "start_ts": "11:00:00Z"},
|
|
],
|
|
}
|
|
end, source = render_dc._derive_session_end(sess)
|
|
self.assertEqual(end, "11:00:00Z")
|
|
self.assertEqual(source, "from SESSION_END interaction")
|
|
|
|
def test_falls_back_to_last_turn_end_ts(self):
|
|
sess = {
|
|
"end_ts": None,
|
|
"interactions": [
|
|
{"type": "TURN", "start_ts": "10:00", "end_ts": "10:01"},
|
|
{"type": "TURN", "start_ts": "10:02", "end_ts": "10:03"},
|
|
],
|
|
}
|
|
end, source = render_dc._derive_session_end(sess)
|
|
self.assertEqual(end, "10:03")
|
|
self.assertEqual(source, "session still open (last TURN)")
|
|
|
|
def test_returns_none_pair_when_no_data(self):
|
|
end, source = render_dc._derive_session_end({"interactions": []})
|
|
self.assertIsNone(end)
|
|
self.assertIsNone(source)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _compose_agent_cell
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class ComposeAgentCellTests(unittest.TestCase):
|
|
|
|
def test_full_identity_concatenated(self):
|
|
out = render_dc._compose_agent_cell({
|
|
"agent_api_name": "MyAgent",
|
|
"agent_version": "v3",
|
|
"agent_label": "My Agent",
|
|
"agent_type": "Internal",
|
|
})
|
|
self.assertEqual(out, "MyAgent v3 — My Agent (Internal)")
|
|
|
|
def test_only_api_name(self):
|
|
out = render_dc._compose_agent_cell({"agent_api_name": "MyAgent"})
|
|
self.assertEqual(out, "MyAgent")
|
|
|
|
def test_only_label(self):
|
|
out = render_dc._compose_agent_cell({"agent_label": "Display Only"})
|
|
self.assertEqual(out, "Display Only")
|
|
|
|
def test_returns_none_for_empty_identity(self):
|
|
self.assertIsNone(render_dc._compose_agent_cell({}))
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _section_session_bootstrap — channel-aware NOT_SET wording
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class SectionSessionBootstrapNotSetWordingTests(unittest.TestCase):
|
|
"""`VariableText__c == NOT_SET` is a "no bootstrap variables" signal,
|
|
not a messaging-channel signal. Several non-messaging shapes (E&O
|
|
headless API, Atlas planner, generic API/integration) legitimately
|
|
produce NOT_SET — the rendered line must not call those "production
|
|
messaging path".
|
|
"""
|
|
|
|
def _identity_with_no_bootstrap(self) -> dict:
|
|
# Sentinel: bootstrap_variables key is present and explicitly None
|
|
# (the assembler stamps None when VariableText__c is NOT_SET).
|
|
return {"bootstrap_variables": None}
|
|
|
|
def test_messaging_channel_keeps_messaging_path_addendum(self):
|
|
out = "\n".join(render_dc._section_session_bootstrap(
|
|
self._identity_with_no_bootstrap(),
|
|
channel="SCRT2 - EmbeddedMessaging",
|
|
))
|
|
self.assertIn("no bootstrap variables", out)
|
|
self.assertIn("production messaging path", out)
|
|
# Alias.
|
|
out2 = "\n".join(render_dc._section_session_bootstrap(
|
|
self._identity_with_no_bootstrap(),
|
|
channel="Messaging",
|
|
))
|
|
self.assertIn("production messaging path", out2)
|
|
|
|
def test_non_messaging_channel_omits_messaging_path(self):
|
|
# E&O is the wheelz that surfaced this bug — Atlas-planner
|
|
# session on the "E & O" channel was being mislabeled
|
|
# "production messaging path".
|
|
out = "\n".join(render_dc._section_session_bootstrap(
|
|
self._identity_with_no_bootstrap(),
|
|
channel="E & O",
|
|
))
|
|
self.assertIn("no bootstrap variables", out)
|
|
self.assertNotIn("production messaging path", out)
|
|
self.assertNotIn("messaging", out.lower())
|
|
|
|
def test_null_channel_uses_neutral_wording(self):
|
|
# No channel info at all — must not assume messaging.
|
|
out = "\n".join(render_dc._section_session_bootstrap(
|
|
self._identity_with_no_bootstrap(),
|
|
channel=None,
|
|
))
|
|
self.assertIn("no bootstrap variables", out)
|
|
self.assertNotIn("production messaging path", out)
|
|
|
|
def test_default_channel_kwarg_is_neutral(self):
|
|
# Backward-compat: callers that don't pass channel get the
|
|
# neutral wording, never the messaging-path label.
|
|
out = "\n".join(render_dc._section_session_bootstrap(
|
|
self._identity_with_no_bootstrap(),
|
|
))
|
|
self.assertIn("no bootstrap variables", out)
|
|
self.assertNotIn("production messaging path", out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|