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.
288 lines
10 KiB
Python
288 lines
10 KiB
Python
"""Tests for ``assemble_dc`` pure helpers — independent of disk fixtures.
|
|
|
|
Complements ``test_assemble_dc_gateway_direct.py`` (binding chain) by
|
|
hitting the small, mechanical helpers that bridge raw DC rows and the
|
|
assembled tree:
|
|
|
|
- ``_clean`` NOT_SET sentinel collapse
|
|
- ``_harvest_str`` unescape + quote-strip + UNSET_VALUE
|
|
- ``_ts`` ISO-8601 → datetime; NOT_SET / non-string → None
|
|
- ``_index_unique`` PK dedup with first-write-wins + collision record
|
|
- ``_groupby`` group rows by FK, dropping NOT_SET keys
|
|
- ``_extract_trace_id`` primary col with HTML-escaped fallback
|
|
- ``_tier`` ACTION < TOPIC < GUARDRAIL < other
|
|
- ``_window_contains`` half-open / closed timestamp interval semantics
|
|
- ``_load`` disk read with malformed-json tolerance
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import unittest
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
|
|
from . import _bootstrap # noqa: F401 — sys.path setup
|
|
|
|
import assemble_dc # type: ignore
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _clean — NOT_SET → None
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class CleanTests(unittest.TestCase):
|
|
|
|
def test_not_set_token_returns_none(self):
|
|
self.assertIsNone(assemble_dc._clean("NOT_SET"))
|
|
|
|
def test_empty_string_returns_none(self):
|
|
self.assertIsNone(assemble_dc._clean(""))
|
|
|
|
def test_actual_value_passes_through(self):
|
|
self.assertEqual(assemble_dc._clean("real value"), "real value")
|
|
self.assertEqual(assemble_dc._clean(42), 42)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _harvest_str — unescape + quote-strip + UNSET_VALUE collapse
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class HarvestStrTests(unittest.TestCase):
|
|
|
|
def test_none_returns_none(self):
|
|
self.assertIsNone(assemble_dc._harvest_str(None))
|
|
|
|
def test_unescapes_html_entities(self):
|
|
self.assertEqual(
|
|
assemble_dc._harvest_str(""0Xx000""),
|
|
"0Xx000",
|
|
)
|
|
|
|
def test_strips_outer_double_quotes(self):
|
|
self.assertEqual(assemble_dc._harvest_str('"value"'), "value")
|
|
|
|
def test_unset_value_collapses_to_none(self):
|
|
self.assertIsNone(assemble_dc._harvest_str("UNSET_VALUE"))
|
|
|
|
def test_not_set_collapses_to_none(self):
|
|
self.assertIsNone(assemble_dc._harvest_str("NOT_SET"))
|
|
|
|
def test_empty_string_collapses_to_none(self):
|
|
self.assertIsNone(assemble_dc._harvest_str(""))
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _ts — ISO-8601 timestamp parsing
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class TsTests(unittest.TestCase):
|
|
|
|
def test_parses_iso_z_timestamp(self):
|
|
out = assemble_dc._ts("2026-04-22T10:00:00Z")
|
|
self.assertEqual(out, datetime(2026, 4, 22, 10, 0, 0, tzinfo=timezone.utc))
|
|
|
|
def test_parses_iso_offset_timestamp(self):
|
|
out = assemble_dc._ts("2026-04-22T10:00:00+00:00")
|
|
self.assertEqual(out, datetime(2026, 4, 22, 10, 0, 0, tzinfo=timezone.utc))
|
|
|
|
def test_returns_none_for_non_string(self):
|
|
self.assertIsNone(assemble_dc._ts(None))
|
|
self.assertIsNone(assemble_dc._ts(12345))
|
|
|
|
def test_returns_none_for_not_set(self):
|
|
self.assertIsNone(assemble_dc._ts("NOT_SET"))
|
|
self.assertIsNone(assemble_dc._ts(""))
|
|
|
|
def test_returns_none_for_unparseable(self):
|
|
self.assertIsNone(assemble_dc._ts("not-a-timestamp"))
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _index_unique
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class IndexUniqueTests(unittest.TestCase):
|
|
|
|
def test_builds_dict_keyed_by_field(self):
|
|
rows = [{"id": "a", "v": 1}, {"id": "b", "v": 2}]
|
|
out = assemble_dc._index_unique(rows, "id", [], dmo_label="t")
|
|
self.assertEqual(set(out.keys()), {"a", "b"})
|
|
|
|
def test_first_write_wins_on_collision(self):
|
|
rows = [{"id": "a", "v": 1}, {"id": "a", "v": 2}]
|
|
collisions: list[dict] = []
|
|
out = assemble_dc._index_unique(rows, "id", collisions, dmo_label="dmo1")
|
|
self.assertEqual(out["a"]["v"], 1)
|
|
self.assertEqual(len(collisions), 1)
|
|
self.assertEqual(collisions[0]["dmo"], "dmo1")
|
|
self.assertEqual(collisions[0]["key"], "a")
|
|
|
|
def test_skips_NOT_SET_keys(self):
|
|
rows = [{"id": "NOT_SET", "v": 1}, {"id": "", "v": 2}, {"id": "a", "v": 3}]
|
|
out = assemble_dc._index_unique(rows, "id", [], dmo_label="t")
|
|
self.assertEqual(set(out.keys()), {"a"})
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _groupby
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class GroupbyTests(unittest.TestCase):
|
|
|
|
def test_groups_rows_by_key(self):
|
|
rows = [{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "a", "v": 3}]
|
|
out = assemble_dc._groupby(rows, "k")
|
|
self.assertEqual([r["v"] for r in out["a"]], [1, 3])
|
|
self.assertEqual([r["v"] for r in out["b"]], [2])
|
|
|
|
def test_skips_NOT_SET_keys(self):
|
|
rows = [{"k": "a", "v": 1}, {"k": "NOT_SET", "v": 2}]
|
|
out = assemble_dc._groupby(rows, "k")
|
|
self.assertNotIn("NOT_SET", out)
|
|
self.assertIn("a", out)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _extract_trace_id
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class ExtractTraceIdTests(unittest.TestCase):
|
|
|
|
def test_uses_primary_column_when_populated(self):
|
|
out = assemble_dc._extract_trace_id({"ssot__TelemetryTraceId__c": "abc"})
|
|
self.assertEqual(out, "abc")
|
|
|
|
def test_falls_back_to_html_escaped_attribute_text(self):
|
|
out = assemble_dc._extract_trace_id({
|
|
"ssot__TelemetryTraceId__c": "",
|
|
"ssot__AttributeText__c": '"internalTraceId":"deadbeef"',
|
|
})
|
|
self.assertEqual(out, "deadbeef")
|
|
|
|
def test_returns_none_when_no_trace_id_anywhere(self):
|
|
out = assemble_dc._extract_trace_id({
|
|
"ssot__TelemetryTraceId__c": "",
|
|
"ssot__AttributeText__c": "no trace here",
|
|
})
|
|
self.assertIsNone(out)
|
|
|
|
def test_returns_none_for_NOT_SET(self):
|
|
out = assemble_dc._extract_trace_id({
|
|
"ssot__TelemetryTraceId__c": "NOT_SET",
|
|
"ssot__AttributeText__c": "",
|
|
})
|
|
self.assertIsNone(out)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _tier — ACTION < TOPIC < GUARDRAIL < other
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class TierTests(unittest.TestCase):
|
|
|
|
def test_action_step_is_lowest_tier(self):
|
|
self.assertLess(
|
|
assemble_dc._tier("ACTION_STEP"),
|
|
assemble_dc._tier("TOPIC_STEP"),
|
|
)
|
|
|
|
def test_topic_step_outranks_guardrails(self):
|
|
self.assertLess(
|
|
assemble_dc._tier("TOPIC_STEP"),
|
|
assemble_dc._tier("TRUST_GUARDRAILS_STEP"),
|
|
)
|
|
|
|
def test_unknown_step_type_falls_to_end(self):
|
|
self.assertEqual(
|
|
assemble_dc._tier("MYSTERY"),
|
|
len(assemble_dc._TIER_ORDER),
|
|
)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _window_contains
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class WindowContainsTests(unittest.TestCase):
|
|
|
|
def _ts(self, h, m, s):
|
|
return datetime(2026, 4, 22, h, m, s, tzinfo=timezone.utc)
|
|
|
|
def test_in_range_inclusive_of_start_and_end(self):
|
|
gw = self._ts(10, 0, 5)
|
|
self.assertTrue(
|
|
assemble_dc._window_contains(gw, self._ts(10, 0, 0), self._ts(10, 0, 10))
|
|
)
|
|
|
|
def test_open_ended_when_end_is_none(self):
|
|
gw = self._ts(11, 0, 0)
|
|
self.assertTrue(
|
|
assemble_dc._window_contains(gw, self._ts(10, 0, 0), None)
|
|
)
|
|
|
|
def test_returns_false_when_start_is_none(self):
|
|
gw = self._ts(10, 0, 0)
|
|
self.assertFalse(
|
|
assemble_dc._window_contains(gw, None, self._ts(11, 0, 0))
|
|
)
|
|
|
|
def test_returns_false_when_before_start(self):
|
|
gw = self._ts(9, 0, 0)
|
|
self.assertFalse(
|
|
assemble_dc._window_contains(gw, self._ts(10, 0, 0), self._ts(11, 0, 0))
|
|
)
|
|
|
|
def test_returns_false_when_after_end(self):
|
|
gw = self._ts(12, 0, 0)
|
|
self.assertFalse(
|
|
assemble_dc._window_contains(gw, self._ts(10, 0, 0), self._ts(11, 0, 0))
|
|
)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _load — disk read with tolerance
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class LoadTests(unittest.TestCase):
|
|
|
|
def test_returns_empty_list_when_file_missing(self):
|
|
with TemporaryDirectory() as t:
|
|
warnings: list[str] = []
|
|
self.assertEqual(
|
|
assemble_dc._load(Path(t), "missing", warnings), []
|
|
)
|
|
# Missing file is normal — no warning recorded.
|
|
self.assertEqual(warnings, [])
|
|
|
|
def test_returns_loaded_rows_when_file_valid(self):
|
|
with TemporaryDirectory() as t:
|
|
tmp = Path(t)
|
|
(tmp / "dc.sessions.json").write_text(json.dumps([{"a": 1}, {"a": 2}]))
|
|
warnings: list[str] = []
|
|
out = assemble_dc._load(tmp, "sessions", warnings)
|
|
self.assertEqual(out, [{"a": 1}, {"a": 2}])
|
|
self.assertEqual(warnings, [])
|
|
|
|
def test_returns_empty_list_and_records_warning_on_malformed_json(self):
|
|
with TemporaryDirectory() as t:
|
|
tmp = Path(t)
|
|
(tmp / "dc.broken.json").write_text("<<<not json>>>")
|
|
warnings: list[str] = []
|
|
out = assemble_dc._load(tmp, "broken", warnings)
|
|
self.assertEqual(out, [])
|
|
self.assertEqual(warnings, ["broken"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|