mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-10 09:41:17 +08:00
Data Cloud 360° view of a single Agentforce session — DC-only, zero
Splunk dependency. Pulls 24 STDM + GenAI DMOs via the Data Cloud Query
REST API, assembles a hierarchical session tree (Interaction → Step →
Generation → GatewayRequest), and renders a human-readable markdown
summary with transcript + per-turn topic/action invocations + LLM
generations + tool calls + audit chain.
Migrated as a standalone Apache-2.0 skill from an internal hub plugin —
self-contained, no sibling-skill or plugin dependencies.
What this skill answers:
- "Trace session <uuid>" / "Summarize what happened in <0Mw…>"
- "Find escalated sessions today on Messaging in <org>"
- Session discovery by time / agent / channel / outcome / conversation
text when the user has no session id
What it does NOT answer (use a different surface):
- Design-time architecture — use investigating-agentforce-architecture
- Runtime planner availability — DC alone can't tell you which
topic/action was eligible for the classifier on a given turn
Skill layout:
- 8 Python pipeline modules (fetch_dc, assemble_dc, render_dc,
discover_sessions, resolve_session, dc, storage, config)
- 4 _shared helpers (paths, fs_guard, sql, __init__) with skill-scoped
DATA_ROOT (~/.claude/data/investigating-agentforce-d360/)
- 26 SQL templates under assets/dc/
- 27 test files (367 tests + 18 subtests, 100% passing)
- 3 reference docs (artifacts.md, dc_dmo_fields.md,
dc_pipeline_contract.md)
- SKILL.md (sf-skills frontmatter, license: Apache-2.0,
metadata.version: "1.0")
- README.md (external-facing quick-start)
- tools/grant_allowlist.py (idempotent first-run permission grant)
- tools/archive_data_dir.sh (opt-in stop-hook tarballer)
Quality gates:
- pytest scripts/tests/: 367 passed + 18 subtests, 0 failures
- npm run validate:skills: 62 of 62 skill(s) checked, 0 errors
- Live end-to-end runs against 3 real Salesforce sessions exercising
both the full-tree and STDM-lag gateway-direct render branches
- 4 independent code-review rounds (correctness, security, markdown,
architecture-critic) — all findings addressed
Customer-data hygiene: no live tenant ids, no internal sprint markers,
no hub/sibling-skill references. Synthetic fixtures look obviously
synthetic (`019dface-…` UUIDs, `0MwTESTMSG…` MessagingSession ids,
`00DTESTORG…` org ids, `MyAgent` placeholder agent name).
Sibling skill: investigating-agentforce-architecture (PR #278) — same
migration pattern, design-time metadata; complementary scope.
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()
|