afv-library/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_helpers.py

288 lines
10 KiB
Python
Raw Normal View History

@W-22707610 feat: add investigating-agentforce-d360 skill 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.
2026-05-28 18:57:52 +08:00
"""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("&quot;0Xx000&quot;"),
"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": '&quot;internalTraceId&quot;:&quot;deadbeef&quot;',
})
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()