agentforce-d360-analyze — Data Cloud 360° session view Hierarchical session reconstruction from Data Cloud STDM + GenAI DMOs for one Agentforce session. Three stages — fetch → assemble → render. Typical wall-clock: 10–30s for a 15-turn session. The pipeline is DC-only : it reads runtime audit rows that Data Cloud has materialized. It is not a runtime-availability tool — see "DC-only blind spot" below for what this skill cannot answer. If the user hasn't given enough to proceed When invoked with no session id AND no discovery criteria, print this block verbatim — do not paraphrase, do not pre-…

) # @rule-suppress starter-sec-002 — re.compile, not eval/exec\n\n# Tier order for timestamp-window fallback. \"any other\" is an implicit last-resort\n# catch-all covering LLM_STEP (without declared binding), SESSION_END, and any\n# future step types not explicitly listed.\n_TIER_ORDER = (\"ACTION_STEP\", \"TOPIC_STEP\", \"TRUST_GUARDRAILS_STEP\")\n\n# Canonical identity-field name → ordered list of `gateway_request_tags.tag__c`\n# values that carry it, tried in order. Agent versions emit different tag\n# names: newer Atlas ReAct agents use `agent_developer_name` /\n# `agent_version_api_name`; legacy MyAgent builds omit the developer\n# name entirely and use the unprefixed `version_api_name`. First non-null\n# value wins. A single-element list means \"no fallback known.\"\n_TAG_KEY_ALIASES: Dict[str, Tuple[str, ...]] = {\n \"agent_api_name\": (\"agent_developer_name\",),\n \"agent_version\": (\"agent_version_api_name\", \"version_api_name\"),\n}\n\n\n# ---- typed namespaces ------------------------------------------------------\n#\n# Four frozen dataclasses replace the former dict-bags. frozen=True guards\n# attribute re-assignment, not mutation of the dict values themselves — the\n# producers are responsible for handing in plain dicts (not defaultdicts) so\n# downstream helpers don't rely on auto-creation the type doesn't promise.\n\n@dataclass(frozen=True)\nclass Indexes:\n interactions_by_id: Dict[str, dict]\n participants_by_id: Dict[str, dict]\n generations_by_id: Dict[str, dict]\n gw_req_by_id: Dict[str, dict]\n gw_resp_by_resp_id: Dict[str, dict]\n feedback_by_id: Dict[str, dict]\n gw_resp_by_req_id: Dict[str, List[dict]]\n steps_by_interaction: Dict[str, List[dict]]\n messages_by_interaction: Dict[str, List[dict]]\n gw_tags_by_parent: Dict[str, List[dict]]\n gw_md_by_parent: Dict[str, List[dict]]\n gw_llm_by_parent: Dict[str, List[dict]]\n quality_by_parent: Dict[str, List[dict]]\n quality_by_id: Dict[str, dict]\n feedback_by_gen: Dict[str, List[dict]]\n feedback_details_by_parent: Dict[str, List[dict]]\n participant_role_by_id: Dict[str, Optional[str]]\n\n\n@dataclass(frozen=True)\nclass PolymorphicSplits:\n categories_by_generation: Dict[str, List[dict]]\n categories_by_quality: Dict[str, List[dict]]\n gw_records_by_gw_req: Dict[str, List[dict]]\n gw_records_by_feedback: Dict[str, List[dict]]\n tag_assoc_session: List[dict]\n tag_assoc_by_interaction: Dict[str, List[dict]]\n tag_assoc_by_moment: Dict[str, List[dict]]\n\n\n@dataclass(frozen=True)\nclass BindingResults:\n declared_gw_ids: Set[str]\n declared_steps_with_gw: frozenset\n step_id_to_gw_id: Dict[str, Optional[str]]\n declared_collisions: int\n\n\n@dataclass(frozen=True)\nclass Catalog:\n agents_observed: List[str]\n tag_definitions: List[dict]\n tag_definition_associations: List[dict]\n tags: List[dict]\n\n\n@dataclass\nclass BinderCtx:\n \"\"\"Per-interaction scratch state used only by the timestamp-window pass.\n\n Kept in a parallel `Dict[iid, BinderCtx]` instead of stashed on the\n interaction view, so binder state can never leak into the emitted tree.\n \"\"\"\n start_ts: Optional[datetime]\n end_ts: Optional[datetime]\n steps_with_ts: List[Tuple[dict, Optional[datetime], Optional[datetime]]]\n reserved_step_ids: frozenset\n\n\n# ---- session-dir resolution ----------------------------------------------\n\ndef _find_session_dir(sid: str) -> Path:\n \"\"\"Locate the session dir under the nested layout.\n\n Given only a session id, we don't know ``(org, agent, version)`` upfront.\n Strategy:\n\n 1. Validate ``sid`` against ``paths.SESSION_ID_RE`` at entry. ``sid`` comes\n in via argv / resolve_session and flows directly into path composition\n (``\u003corg>/_sessions/\u003csid>.link``) and glob patterns; an unvalidated\n value here would undo the traversal guard added in PR #657 BLOCKER-2.\n 2. Breadcrumb lookup: ``DATA_ROOT/\u003corg>/_sessions/\u003csid>.link`` is a plain-\n text relative-path pointer written by ``storage.save``. Iterate all\n orgs, read each ``.link``, resolve against the breadcrumb's parent,\n and **enforce containment** — a tampered or stale breadcrumb whose\n target escapes ``DATA_ROOT`` is skipped (not raised) so a single\n malicious breadcrumb can't DoS the whole resolver. Falls through to\n the glob fallback on any breadcrumb miss.\n 3. Glob fallback: ``DATA_ROOT/*/*/\u003csid>/`` — ``sid`` is now validated at\n entry so the pattern is fixed-depth and cannot glob outside its\n intended 2-level subtree.\n 4. Raise ``SystemExit`` with a clear hint to run fetch_dc.py first.\n\n Returns the absolute directory path.\n \"\"\"\n # Validate first — rejects \"../etc\", \"a/b\", \"\", None, and control chars.\n # ``sid`` from here on is safe to use as a path segment and as the tail\n # of a fixed-depth glob.\n paths.validate_session_id(sid)\n root = paths.DATA_ROOT\n if root.is_dir():\n # Resolve DATA_ROOT once so containment checks don't repeat the walk.\n root_resolved = root.resolve()\n for org_dir in root.iterdir():\n if not org_dir.is_dir():\n continue\n link = org_dir / \"_sessions\" / f\"{sid}.link\"\n if link.is_file():\n try:\n rel = link.read_text().strip()\n except OSError:\n continue\n # Resolve relative to the breadcrumb's parent (_sessions/),\n # then enforce that the resolved path stays inside\n # DATA_ROOT. ``.link`` contents are user-writable — a planted\n # breadcrumb with ``../../../../etc/passwd`` must NOT pivot\n # the assembler outside the plugin's data tree.\n target = (link.parent / rel).resolve()\n if not target.is_relative_to(root_resolved):\n # Stale or malicious breadcrumb. Skip — fall through to\n # the glob fallback rather than raising, so one bad\n # breadcrumb in one org doesn't block discovery in\n # another.\n continue\n if target.is_dir():\n return target\n # Glob fallback. ``sid`` is validated; the pattern is fixed-depth.\n matches = list(root.glob(f\"*/*/{sid}\"))\n if len(matches) == 1:\n return matches[0]\n if len(matches) > 1:\n # Placeholder agent dirs (leading-underscore name like\n # ``\u003corg>/_agent_\u003cbotid>__v0/``) mark provisional sessions\n # whose identity wasn't fully resolved at first write. When a\n # real (agent, version) dir also exists for the same session,\n # it's the authoritative home; the placeholder is stale.\n # Prefer the real dir.\n real = [p for p in matches if not p.parent.name.startswith(\"_\")]\n if len(real) == 1:\n return real[0]\n raise SystemExit(\n f\"assemble_dc: session {sid} resolves to {len(matches)} dirs \"\n f\"under {root} — ambiguous. Check for duplicate sessions \"\n f\"across agents.\"\n )\n raise SystemExit(\n f\"assemble_dc: session dir for {sid} not found under {root}; \"\n f\"run fetch_dc.py first\"\n )\n\n\n# ---- loaders ---------------------------------------------------------------\n\ndef _load(session_dir: Path, name: str, parse_warnings: List[str]) -> List[dict]:\n \"\"\"Load dc.\u003cname>.json from session_dir. Missing → []. Malformed → [] + warning.\"\"\"\n p = session_dir / f\"dc.{name}.json\"\n if not p.is_file():\n return []\n try:\n return json.loads(p.read_text())\n except (json.JSONDecodeError, OSError) as e:\n print(f\"assemble_dc: WARN dc.{name}.json unreadable: {str(e).splitlines()[0]}\",\n file=sys.stderr)\n parse_warnings.append(name)\n return []\n\n\ndef _load_manifest(session_dir: Path) -> dict:\n p = session_dir / \"dc._session_manifest.json\"\n if not p.is_file():\n raise SystemExit(\n f\"assemble_dc: manifest not found at {p}; run fetch_dc.py first\"\n )\n manifest = json.loads(p.read_text())\n if \"queries\" not in manifest:\n raise SystemExit(\"assemble_dc: manifest schema changed — no 'queries' key\")\n return manifest\n\n\ndef _load_all(sid: str) -> Tuple[dict, Dict[str, List[dict]], List[str], Path]:\n \"\"\"Return (manifest, rows_by_name, parse_warnings, session_dir).\n\n Iterates manifest[\"queries\"][*][\"name\"] rather than a hard-coded list —\n a new DMO added to fetch_dc.py is picked up automatically.\n \"\"\"\n session_dir = _find_session_dir(sid)\n manifest = _load_manifest(session_dir)\n parse_warnings: List[str] = []\n rows = {\n q[\"name\"]: _load(session_dir, q[\"name\"], parse_warnings)\n for q in manifest[\"queries\"]\n }\n return manifest, rows, parse_warnings, session_dir\n\n\n# ---- small helpers ---------------------------------------------------------\n\ndef _clean(value: Any) -> Any:\n \"\"\"NOT_SET sentinel → None. Other values pass through.\"\"\"\n return None if value in _NOT_SET else value\n\n\ndef _harvest_str(value: Any) -> Optional[str]:\n \"\"\"Harvest-layer string normalizer for the session-identity block.\n\n Handles three quirks that `_clean` deliberately does not:\n 1. **html.unescape** — tag values arrive double-escaped\n (`\""0Xx…"\"`).\n 2. **Quote-strip** — after unescape most tag values are wrapped in\n literal `\"` characters; strip them.\n 3. **`UNSET_VALUE` sentinel** — Data Cloud emits this on a small set\n of optional columns (e.g. `gateway_requests.promptTemplateVersionNo__c`,\n certain `tag_first` values on cold-start sessions). Not observed\n on the columns `_build_session_identity` currently reads, but\n included defensively since the sentinel is part of the DC schema\n contract and a harvest-layer reader should collapse it to None.\n\n The binding / index layer uses `_clean()` / `_NOT_SET` which\n intentionally omits these rules — they would be noise there.\n \"\"\"\n if value is None:\n return None\n s = html.unescape(str(value)).strip()\n if len(s) >= 2 and s.startswith('\"') and s.endswith('\"'):\n s = s[1:-1]\n return None if s in (\"\", \"NOT_SET\", \"UNSET_VALUE\") else s\n\n\ndef _promote_identity(\n manifest_value: Any,\n session_identity_value: Any,\n *,\n kind: Literal[\"api_name\", \"version\"],\n) -> Any:\n \"\"\"Pick the richer of (manifest, session_identity) for a top-level identity slot.\n\n Background. ``fetch_dc._resolve_identity`` walks AGENT-role participant\n rows for ``(api_name, version)``. On agent shapes like MyAgent the\n AGENT rows can leave both fields NOT_SET, so the resolver falls back\n (fetch_dc.py:570-597) to picking ``api_name`` from any participant row\n and stamping ``version=\"v0\"`` as a placeholder satisfying\n ``paths.session_dir``'s ``^v[0-9]+ agentforce-d360-analyze — Skillopedia ` shape. The placeholder is enough\n to land the session in a directory, but it's wrong: by wave 5 the\n fetch has materialized ``gateway_request_tags`` rows carrying the real\n ``agent_version_api_name`` (e.g. ``v24``), and ``_build_session_identity``\n correctly harvests it onto ``session.identity``. The top-level\n ``identity`` block was previously copied verbatim from the manifest,\n so the placeholder leaked downstream while the right value sat\n visible in the same JSON.\n\n Policy:\n\n - ``kind=\"version\"``: if manifest is the ``\"v0\"`` placeholder AND\n session_identity carries a non-``v0`` value matching ``^v[0-9]+ agentforce-d360-analyze — Skillopedia `,\n promote. Otherwise keep manifest.\n - ``kind=\"api_name\"``: if manifest is NOT_SET-ish (None / \"\" / \"NOT_SET\"\n / \"NOT SET\") AND session_identity has a real value, promote.\n Otherwise keep manifest. (Crucially, when session_identity is None\n and manifest has a value, we keep manifest — the strict AGENT-row\n pick is intentional on healthy sessions.)\n - When manifest and session_identity both carry real-but-disagreeing\n values, the manifest wins. The strict AGENT-row pick is the\n authoritative source on a normal session; we only promote in the\n narrow case where the manifest carries a known placeholder /\n NOT_SET sentinel and the harvest layer has something better.\n \"\"\"\n if kind == \"version\":\n if manifest_value != \"v0\":\n return manifest_value\n if not isinstance(session_identity_value, str):\n return manifest_value\n if session_identity_value == \"v0\":\n return manifest_value\n if not _REAL_VERSION_RE.match(session_identity_value):\n return manifest_value\n return session_identity_value\n # kind == \"api_name\"\n if manifest_value not in (None, \"\", \"NOT_SET\", \"NOT SET\"):\n return manifest_value\n if isinstance(session_identity_value, str) and session_identity_value:\n return session_identity_value\n return manifest_value\n\n\ndef _reconcile_top_identity(\n manifest: dict, session_identity: dict, org_id_15: Any,\n) -> dict:\n \"\"\"Build the top-level ``identity`` block, promoting placeholders.\n\n Centralizes the policy in one place so the happy path\n (`_assemble_session`) and the gateway-direct fallback don't drift\n apart. Emits a stderr note when promotion fires so an investigator\n sees the divergence at run time.\n \"\"\"\n manifest_api = manifest.get(\"agent_api_name\")\n manifest_ver = manifest.get(\"agent_version\")\n session_api = session_identity.get(\"agent_api_name\")\n session_ver = session_identity.get(\"agent_version\")\n\n promoted_api = _promote_identity(manifest_api, session_api, kind=\"api_name\")\n promoted_ver = _promote_identity(manifest_ver, session_ver, kind=\"version\")\n\n if promoted_api != manifest_api:\n print(\n f\"assemble_dc: identity promoted: agent_api_name \"\n f\"{manifest_api!r} -> {promoted_api!r} (from session.identity)\",\n file=sys.stderr,\n )\n if promoted_ver != manifest_ver:\n print(\n f\"assemble_dc: identity promoted: agent_version \"\n f\"{manifest_ver} -> {promoted_ver} (from session.identity)\",\n file=sys.stderr,\n )\n\n return {\n \"org_id_15\": org_id_15,\n \"agent_api_name\": promoted_api,\n \"agent_version\": promoted_ver,\n }\n\n\ndef _resolve_end_type(session_row: dict, rows: dict) -> Optional[str]:\n \"\"\"Resolve the session's terminal outcome with a Session→Step fallback.\n\n Session DMO's ``ssot__AiAgentSessionEndType__c`` is authoritative when\n populated, but on Messaging-channel and short E&O sessions it stays\n ``NOT_SET`` even after the SESSION_END interaction has materialized.\n The runtime writes the actual outcome (``CLOSED_USER_REQUEST``,\n ``USER_ENDED``, ``ESCALATED``, ``TRANSFERRED``, ``TIMEOUT``) onto the\n SESSION_END step's ``ssot__Name__c`` instead. Fall through to that\n step when Session.EndType is missing, so the rendered summary stops\n saying \"session end not yet materialized in STDM\" for sessions that\n actually completed cleanly.\n \"\"\"\n primary = _clean(session_row.get(\"ssot__AiAgentSessionEndType__c\"))\n if primary:\n return primary\n for step in rows.get(\"steps\", []) or ():\n if step.get(\"ssot__AiAgentInteractionStepType__c\") == \"SESSION_END\":\n name = _clean(step.get(\"ssot__Name__c\"))\n if name:\n return name\n return None\n\n\ndef _ts(value: Any) -> Optional[datetime]:\n \"\"\"Parse an ISO-8601 timestamp. NOT_SET / non-string → None (unbounded).\"\"\"\n if not isinstance(value, str) or value in _NOT_SET:\n return None\n try:\n return datetime.fromisoformat(value.replace(\"Z\", \"+00:00\"))\n except ValueError:\n return None\n\n\ndef _index_unique(rows: Iterable[dict], key: str,\n collisions: List[dict], dmo_label: str) -> Dict[str, dict]:\n \"\"\"Build a {key_value: row} dict. On collision: first-write-wins + record.\"\"\"\n out: Dict[str, dict] = {}\n for r in rows:\n k = r.get(key)\n if k in _NOT_SET:\n continue\n if k in out:\n collisions.append({\"dmo\": dmo_label, \"key\": k})\n else:\n out[k] = r\n return out\n\n\ndef _groupby(rows: Iterable[dict], key: str) -> Dict[str, List[dict]]:\n out: Dict[str, List[dict]] = defaultdict(list)\n for r in rows:\n k = r.get(key)\n if k not in _NOT_SET:\n out[k].append(r)\n return dict(out)\n\n\ndef _extract_trace_id(interaction: dict) -> Optional[str]:\n \"\"\"Prefer the primary column; fall back to AttributeText regex.\"\"\"\n tid = interaction.get(\"ssot__TelemetryTraceId__c\")\n if tid and tid not in _NOT_SET:\n return tid\n attr = interaction.get(\"ssot__AttributeText__c\") or \"\"\n if not attr:\n return None\n m = _INTERNAL_TRACE_RE.search(html.unescape(attr))\n return m.group(1) if m else None\n\n\n# ---- declared binding chain ------------------------------------------------\n\ndef _declared_gw_for_step(step: dict,\n generations_by_id: Dict[str, dict],\n gw_resp_by_resp_id: Dict[str, dict],\n gw_req_by_id: Dict[str, dict]) -> Optional[dict]:\n \"\"\"Step → Generation → Response → Request. Returns the GatewayRequest row or None.\"\"\"\n gen_id = step.get(\"ssot__GenerationId__c\")\n if gen_id in _NOT_SET:\n return None\n gen = generations_by_id.get(gen_id)\n if not gen:\n return None\n resp_id = gen.get(\"generationResponseId__c\")\n if resp_id in _NOT_SET:\n return None\n resp = gw_resp_by_resp_id.get(resp_id)\n if not resp:\n return None\n req_id = resp.get(\"generationRequestId__c\")\n if req_id in _NOT_SET:\n return None\n return gw_req_by_id.get(req_id)\n\n\n# ---- timestamp-window fallback --------------------------------------------\n\ndef _tier(step_type: str) -> int:\n \"\"\"Lower is better. 0 = ACTION, 1 = TOPIC, 2 = GUARDRAIL, 3 = any other.\"\"\"\n try:\n return _TIER_ORDER.index(step_type)\n except ValueError:\n return len(_TIER_ORDER) # \"any other\"\n\n\ndef _window_contains(gw_ts: datetime, start: Optional[datetime],\n end: Optional[datetime]) -> bool:\n \"\"\"Closed-closed containment. None end_ts → +∞. Missing start → no match.\"\"\"\n if start is None:\n return False\n if gw_ts \u003c start:\n return False\n if end is None:\n return True # open-ended upward\n return gw_ts \u003c= end\n\n\ndef _bind_ts_window(gw_req: dict,\n steps_with_ts: List[Tuple[dict, datetime, Optional[datetime]]],\n interaction_window: Tuple[Optional[datetime], Optional[datetime]],\n reserved_step_ids: \"frozenset[str]\") -> Tuple[str, Optional[str]]:\n \"\"\"Return (placement, bound_step_id).\n\n placement ∈ {\"step\", \"interaction\", \"unbound\"}.\n bound_step_id is set only when placement == \"step\".\n \"\"\"\n gw_ts_raw = gw_req.get(\"timestamp__c\")\n gw_ts = _ts(gw_ts_raw)\n if gw_ts is None:\n return (\"unbound\", None)\n\n # Step candidates: contains gw_ts AND not already declared-bound.\n candidates = [\n (step, start, end)\n for step, start, end in steps_with_ts\n if step[\"ssot__Id__c\"] not in reserved_step_ids\n and _window_contains(gw_ts, start, end)\n ]\n if candidates:\n # Best tier → innermost (shortest window) → latest start_ts.\n def sort_key(c):\n step, start, end = c\n tier = _tier(step.get(\"ssot__AiAgentInteractionStepType__c\", \"\"))\n # Window size; treat None end_ts as \"longest\" (so nested closed wins).\n if end is None:\n width = float(\"inf\")\n else:\n width = (end - start).total_seconds()\n # Invert latest-start-wins by negating seconds since epoch.\n return (tier, width, -start.timestamp())\n candidates.sort(key=sort_key)\n winner = candidates[0][0]\n return (\"step\", winner[\"ssot__Id__c\"])\n\n # Interaction window.\n i_start, i_end = interaction_window\n if _window_contains(gw_ts, i_start, i_end):\n return (\"interaction\", None)\n return (\"unbound\", None)\n\n\n# ---- gateway-request view builder -----------------------------------------\n\ndef _build_gw_view(gw_req: dict, binding_method: str, *,\n idx: Indexes, dispatch: PolymorphicSplits,\n bound_step_id: Optional[str] = None) -> dict:\n \"\"\"Build one GatewayRequest view row.\n\n `idx` and `dispatch` are kw-only so `functools.partial(_build_gw_view,\n idx=..., dispatch=...)` bindings don't collide with positional args at\n the 3 call sites (declared / timestamp_window / unbound).\n \"\"\"\n gw_id = gw_req[\"gatewayRequestId__c\"]\n responses = idx.gw_resp_by_req_id.get(gw_id, [])\n view: Dict[str, Any] = {\n \"binding_method\": binding_method,\n \"gateway_request_id\": gw_id,\n \"feature\": _clean(gw_req.get(\"feature__c\")),\n \"model\": _clean(gw_req.get(\"model__c\")),\n \"provider\": _clean(gw_req.get(\"provider__c\")),\n \"prompt_template_dev_name\": _clean(gw_req.get(\"promptTemplateDevName__c\")),\n \"prompt_tokens\": gw_req.get(\"promptTokens__c\"),\n \"completion_tokens\": gw_req.get(\"completionTokens__c\"),\n \"total_tokens\": gw_req.get(\"totalTokens__c\"),\n # Carry the raw input prompt through the hierarchical view so the\n # renderer can surface it in the opt-in \"Planner LLM calls\" section.\n # The 64 KB display cap lives in render_dc, not here — the tree\n # stores the authoritative payload.\n \"prompt_text\": gw_req.get(\"prompt__c\"),\n \"response\": responses[0] if responses else None,\n \"tags\": idx.gw_tags_by_parent.get(gw_id, []),\n \"records\": dispatch.gw_records_by_gw_req.get(gw_id, []),\n \"metadata\": idx.gw_md_by_parent.get(gw_id, []),\n \"llm\": idx.gw_llm_by_parent.get(gw_id, []),\n }\n if bound_step_id is not None:\n view[\"bound_to_step_id\"] = bound_step_id\n return view\n\n\n# ---- main assembly ---------------------------------------------------------\n\ndef _build_indexes(rows: Dict[str, List[dict]], collisions: List[dict]) -> Indexes:\n \"\"\"Build all primary-key dicts and groupby tables. Returns a frozen Indexes.\"\"\"\n return Indexes(\n interactions_by_id=_index_unique(\n rows.get(\"interactions\", []), \"ssot__Id__c\", collisions, \"interactions_by_id\"),\n participants_by_id=_index_unique(\n rows.get(\"participants\", []), \"ssot__Id__c\", collisions, \"participants_by_id\"),\n generations_by_id=_index_unique(\n rows.get(\"generations\", []), \"generationId__c\", collisions, \"generations_by_id\"),\n gw_req_by_id=_index_unique(\n rows.get(\"gateway_requests\", []), \"gatewayRequestId__c\", collisions, \"gw_req_by_id\"),\n gw_resp_by_resp_id=_index_unique(\n rows.get(\"gateway_responses\", []), \"generationResponseId__c\",\n collisions, \"gw_resp_by_resp_id\"),\n feedback_by_id=_index_unique(\n rows.get(\"feedback\", []), \"feedbackId__c\", collisions, \"feedback_by_id\"),\n gw_resp_by_req_id=_groupby(rows.get(\"gateway_responses\", []), \"generationRequestId__c\"),\n steps_by_interaction=_groupby(rows.get(\"steps\", []), \"ssot__AiAgentInteractionId__c\"),\n messages_by_interaction=_groupby(rows.get(\"messages\", []), \"ssot__AiAgentInteractionId__c\"),\n gw_tags_by_parent=_groupby(rows.get(\"gateway_request_tags\", []), \"parent__c\"),\n gw_md_by_parent=_groupby(rows.get(\"gateway_request_metadata\", []), \"parent__c\"),\n gw_llm_by_parent=_groupby(rows.get(\"gateway_request_llm\", []), \"parent__c\"),\n quality_by_parent=_groupby(rows.get(\"content_quality\", []), \"parent__c\"),\n quality_by_id={q[\"id__c\"]: q for q in rows.get(\"content_quality\", []) if q.get(\"id__c\")},\n feedback_by_gen=_groupby(rows.get(\"feedback\", []), \"generationId__c\"),\n feedback_details_by_parent=_groupby(rows.get(\"feedback_details\", []), \"parent__c\"),\n participant_role_by_id={\n p[\"ssot__Id__c\"]: p.get(\"ssot__AiAgentSessionParticipantRole__c\")\n for p in rows.get(\"participants\", []) if p.get(\"ssot__Id__c\")\n },\n )\n\n\ndef _dispatch_polymorphic(rows: Dict[str, List[dict]], idx: Indexes) -> PolymorphicSplits:\n \"\"\"Split ContentCategory, GtwyObjRecord, and TagAssociation by polymorphic parent.\n\n Producers accumulate into defaultdicts for ergonomics, but the returned\n dataclass stores plain dicts — frozen `Dict[...]` typing can't promise\n auto-creation, so don't let it leak.\n \"\"\"\n # ContentCategory: parent is either a generation or a quality row.\n cat_by_gen: Dict[str, List[dict]] = defaultdict(list)\n cat_by_qual: Dict[str, List[dict]] = defaultdict(list)\n for cat in rows.get(\"content_category\", []):\n parent = cat.get(\"parent__c\")\n if parent in _NOT_SET:\n continue\n if parent in idx.generations_by_id:\n cat_by_gen[parent].append(cat)\n elif parent in idx.quality_by_id:\n cat_by_qual[parent].append(cat)\n\n # GtwyObjRecord: parent is either a gateway_request or a feedback row.\n rec_by_gw: Dict[str, List[dict]] = defaultdict(list)\n rec_by_fb: Dict[str, List[dict]] = defaultdict(list)\n for rec in rows.get(\"gateway_records\", []):\n parent = rec.get(\"parent__c\")\n if parent in _NOT_SET:\n continue\n if parent in idx.gw_req_by_id:\n rec_by_gw[parent].append(rec)\n elif parent in idx.feedback_by_id:\n rec_by_fb[parent].append(rec)\n\n # TagAssociation: exactly one of session/interaction/moment FK is populated.\n ta_session: List[dict] = []\n ta_by_int: Dict[str, List[dict]] = defaultdict(list)\n ta_by_mom: Dict[str, List[dict]] = defaultdict(list)\n for ta in rows.get(\"tag_associations\", []):\n if ta.get(\"ssot__AiAgentSessionId__c\") not in _NOT_SET:\n ta_session.append(ta)\n elif ta.get(\"ssot__AiAgentInteractionId__c\") not in _NOT_SET:\n ta_by_int[ta[\"ssot__AiAgentInteractionId__c\"]].append(ta)\n elif ta.get(\"ssot__AiAgentMomentId__c\") not in _NOT_SET:\n ta_by_mom[ta[\"ssot__AiAgentMomentId__c\"]].append(ta)\n\n return PolymorphicSplits(\n categories_by_generation=dict(cat_by_gen),\n categories_by_quality=dict(cat_by_qual),\n gw_records_by_gw_req=dict(rec_by_gw),\n gw_records_by_feedback=dict(rec_by_fb),\n tag_assoc_session=ta_session,\n tag_assoc_by_interaction=dict(ta_by_int),\n tag_assoc_by_moment=dict(ta_by_mom),\n )\n\n\ndef _filter_catalog(rows: Dict[str, List[dict]]) -> Catalog:\n \"\"\"Filter org-wide tag vocabulary to only what's reachable from session agents.\"\"\"\n agents = {\n p.get(\"ssot__AiAgentApiName__c\") for p in rows.get(\"participants\", [])\n if p.get(\"ssot__AiAgentSessionParticipantRole__c\") == \"AGENT\"\n and p.get(\"ssot__AiAgentApiName__c\") not in _NOT_SET\n } | {\n m.get(\"ssot__AiAgentApiName__c\") for m in rows.get(\"moments\", [])\n if m.get(\"ssot__AiAgentApiName__c\") not in _NOT_SET\n }\n # Mirror fetch_dc._resolve_identity's USER-row fallback. On agent shapes\n # like MyAgent, AGENT-role rows leave api_name=NOT_SET while USER\n # rows correctly carry the agent's api_name. Without this fallback, the\n # session lands in a `\u003capi_name>__v0/` directory but `agents_observed`\n # is empty — directory and rendered catalog disagree. Only fires when the\n # primary (AGENT + moments) sources turned up nothing usable; in normal\n # sessions the USER and AGENT rows agree and the union is idempotent.\n if not agents:\n agents = {\n p.get(\"ssot__AiAgentApiName__c\") for p in rows.get(\"participants\", [])\n if p.get(\"ssot__AiAgentApiName__c\") not in _NOT_SET\n }\n agents_observed = sorted(a for a in agents if a)\n relevant_assocs = [\n a for a in rows.get(\"tag_definition_associations\", [])\n if a.get(\"ssot__AiAgentApiName__c\") in agents_observed\n ]\n relevant_def_ids = {\n a[\"ssot__AiAgentTagDefinitionId__c\"] for a in relevant_assocs\n if a.get(\"ssot__AiAgentTagDefinitionId__c\") not in _NOT_SET\n }\n return Catalog(\n agents_observed=agents_observed,\n tag_definitions=[d for d in rows.get(\"tag_definitions\", [])\n if d.get(\"ssot__Id__c\") in relevant_def_ids],\n tag_definition_associations=relevant_assocs,\n tags=[t for t in rows.get(\"tags\", [])\n if t.get(\"ssot__AiAgentTagDefinitionId__c\") in relevant_def_ids],\n )\n\n\ndef _build_session_identity(rows: Dict[str, List[dict]], manifest: dict) -> dict:\n \"\"\"Harvest 18 identity fields from 4 DMOs for the `session.identity` block.\n\n All row iteration is preceded by a deterministic sort so repeated runs\n produce byte-identical output regardless of fetch order. `_harvest_str()` is\n the shared normalizer — html.unescape + quote-strip + NOT_SET /\n UNSET_VALUE / empty coercion. See references/dc_pipeline_contract.md\n §2.9a for the field-to-column mapping.\n \"\"\"\n # --- gateway_requests: sort by (timestamp__c, gatewayRequestId__c) ---\n gwr_sorted = sorted(\n rows.get(\"gateway_requests\", []),\n key=lambda r: (r.get(\"timestamp__c\") or \"\",\n r.get(\"gatewayRequestId__c\") or \"\"),\n )\n\n def _first_gwr(key: str) -> Optional[str]:\n for r in gwr_sorted:\n v = _harvest_str(r.get(key))\n if v is not None:\n return v\n return None\n\n org_id = _first_gwr(\"orgId__c\")\n platform_user_id = _first_gwr(\"userId__c\")\n planner_id = _first_gwr(\"plannerId__c\")\n bot_version_id = _first_gwr(\"botVersionId__c\")\n app_type = _first_gwr(\"appType__c\")\n\n # --- gateway_request_tags: sort by (parent__c, tag__c, tagValue__c) ---\n tags_sorted = sorted(\n rows.get(\"gateway_request_tags\", []),\n key=lambda r: (r.get(\"parent__c\") or \"\",\n r.get(\"tag__c\") or \"\",\n r.get(\"tagValue__c\") or \"\"),\n )\n tag_first: Dict[str, Optional[str]] = {}\n for t in tags_sorted:\n k = t.get(\"tag__c\")\n if not k or k in tag_first:\n continue\n tag_first[k] = _harvest_str(t.get(\"tagValue__c\"))\n\n # --- sessions[0] — always exactly one row per session on this path ---\n sessions = rows.get(\"sessions\", [])\n session_row = sessions[0] if sessions else {}\n\n # --- participants: first USER-role row by ssot__Id__c ---\n participants_sorted = sorted(\n rows.get(\"participants\", []),\n key=lambda r: r.get(\"ssot__Id__c\") or \"\",\n )\n messaging_end_user_id = None\n for p in participants_sorted:\n if p.get(\"ssot__AiAgentSessionParticipantRole__c\") == \"USER\":\n v = _harvest_str(p.get(\"ssot__ParticipantId__c\"))\n if v is not None:\n messaging_end_user_id = v\n break\n\n def _aliased(identity_key: str) -> Optional[str]:\n \"\"\"Resolve an identity field via its tag-name fallback chain.\n\n Parameter name intentionally avoids shadowing `dataclasses.field`.\n \"\"\"\n for tag_key in _TAG_KEY_ALIASES[identity_key]:\n v = tag_first.get(tag_key)\n if v is not None:\n return v\n return None\n\n # Expose VariableText__c bootstrap variables. Production messaging\n # sessions leave this NOT_SET; Builder Previewer populates it with\n # test-harness keys (__resolved_locale__, __supports_result_display__,\n # etc.). Surfacing this is what makes channel-mode visible in the\n # renderer.\n messaging_session_id = _harvest_str(session_row.get(\"ssot__RelatedMessagingSessionId__c\"))\n voice_call_id = _harvest_str(session_row.get(\"ssot__RelatedVoiceCallId__c\"))\n bootstrap_variables = _parse_bootstrap_variables(\n session_row.get(\"ssot__VariableText__c\")\n )\n\n # Derive a `mode` field. ssot__AiAgentChannelType__c is identical for\n # MIAW production and Builder Previewer (`SCRT2 - EmbeddedMessaging`);\n # we have to look at related-id population and bootstrap_variables to\n # tell them apart.\n mode = _derive_mode(messaging_session_id, voice_call_id, bootstrap_variables)\n\n # `voice_call_id` + `individual_id` are null on EmbeddedMessaging sessions.\n # They populate on authenticated channels (voice, Experience Cloud with\n # linked Individual). Kept for schema parallelism with messaging_session_id.\n return {\n \"org_id\": org_id,\n \"platform_user_id\": platform_user_id,\n \"planner_id\": planner_id,\n \"bot_version_id\": bot_version_id,\n \"app_type\": app_type,\n \"bot_id\": tag_first.get(\"bot_id\"),\n \"bot_name\": tag_first.get(\"bot_name\"),\n \"agent_api_name\": _aliased(\"agent_api_name\"),\n \"agent_label\": tag_first.get(\"agent_label\"),\n \"agent_version\": _aliased(\"agent_version\"),\n \"agent_type\": tag_first.get(\"agent_type\"),\n \"planner_name\": tag_first.get(\"planner_name\"),\n \"planner_type\": tag_first.get(\"planner_type\"),\n \"configured_model\": tag_first.get(\"configured_model_name\"),\n \"messaging_session_id\": messaging_session_id,\n \"messaging_end_user_id\": messaging_end_user_id,\n \"voice_call_id\": voice_call_id,\n \"individual_id\": _harvest_str(session_row.get(\"ssot__IndividualId__c\")),\n \"bootstrap_variables\": bootstrap_variables,\n \"mode\": mode,\n }\n\n\n# Test-harness bootstrap keys that are observed in Builder Previewer sessions\n# but NOT in MIAW production. The presence of any of these in\n# `ssot__VariableText__c` is the strongest at-rest signal that a session was\n# run through the Previewer rather than against a real customer messaging\n# session. Listed here as a frozenset so it's read-only at module level.\n# Builder Previewer adds these keys to ssot__VariableText__c at session\n# bootstrap; MIAW production sessions don't seed them. Used by _derive_mode\n# to distinguish previewer runs from real customer messaging sessions.\n_BUILDER_PREVIEWER_INDICATOR_KEYS: frozenset[str] = frozenset({\n \"__resolved_locale__\",\n \"__locale_instruction__\",\n \"__supports_result_display__\",\n \"__show_tool_results_invoked__\",\n})\n\n\ndef _parse_bootstrap_variables(raw: Any) -> Optional[dict]:\n \"\"\"Parse `ssot__VariableText__c` defensively.\n\n On real sessions this field can be:\n - missing / None / NOT_SET / UNSET_VALUE → returns None\n - well-formed JSON (Builder Previewer) → returns the dict\n - HTML-entity-encoded JSON (some surfaces emit\n the `"`-escaped form) → unescaped, returns the dict\n - truncated or malformed JSON → returns\n `{\"_parse_error\": True, \"_raw\": \u003cfirst 200 chars>}` so the renderer\n can still flag that a bootstrap exists, just not parseable.\n\n Returns None for the empty cases so the caller can treat None as\n \"no bootstrap\" without distinguishing missing from sentinel.\n \"\"\"\n if raw is None:\n return None\n s = html.unescape(str(raw)).strip()\n if not s or s in _NOT_SET or s in (\"NOT_SET\", \"UNSET_VALUE\"):\n return None\n try:\n parsed = json.loads(s)\n except (json.JSONDecodeError, ValueError):\n return {\"_parse_error\": True, \"_raw\": s[:200]}\n # Defensive: VariableText__c is documented as a JSON object; if a future\n # version emits a JSON array or scalar, surface it under `_raw` rather\n # than letting downstream code crash on `.get()`.\n if not isinstance(parsed, dict):\n return {\"_parse_error\": True, \"_raw\": s[:200]}\n return parsed\n\n\ndef _derive_mode(\n messaging_session_id: Optional[str],\n voice_call_id: Optional[str],\n bootstrap_variables: Optional[dict],\n) -> str:\n \"\"\"Distinguish MIAW production from Builder Previewer from voice.\n\n `ssot__AiAgentChannelType__c` is identical (`SCRT2 - EmbeddedMessaging`)\n for MIAW and Builder Previewer — useless for distinguishing them. The\n real signals, in priority order:\n\n 1. `ssot__RelatedVoiceCallId__c` set ↔ voice channel.\n 2. `ssot__RelatedMessagingSessionId__c` set ↔ MIAW production\n (a real MessagingSession record exists).\n 3. `RelatedMessagingSessionId__c` NOT_SET AND bootstrap_variables\n contains test-harness keys ↔ Builder Previewer.\n 4. None of the above ↔ unknown (e.g. headless API runs, agent script\n previewer cases that don't seed VariableText__c).\n\n Return a stable enum string the renderer can match on.\n \"\"\"\n if voice_call_id:\n return \"voice\"\n if messaging_session_id:\n return \"production_messaging\"\n if isinstance(bootstrap_variables, dict):\n if set(bootstrap_variables) & _BUILDER_PREVIEWER_INDICATOR_KEYS:\n return \"builder_previewer\"\n return \"unknown\"\n\n\ndef _declared_binding_pass(rows: Dict[str, List[dict]], idx: Indexes) -> BindingResults:\n \"\"\"Walk every step; claim GWs reachable via the declared chain.\n\n Returns BindingResults(declared_gw_ids, declared_steps_with_gw,\n step_id_to_gw_id, declared_collisions). `declared_steps_with_gw` is a\n frozenset so downstream consumers can't accidentally mutate it.\n `step_id_to_gw_id[step_id]` is the GW id when declared, or None when this\n step's declared GW was already claimed by an earlier step (collision sentinel).\n `declared_collisions` is the aggregate count of collision-sentinel entries.\n \"\"\"\n declared_gw_ids: set = set()\n declared_steps_with_gw: set = set()\n step_id_to_gw_id: Dict[str, Optional[str]] = {}\n for step in rows.get(\"steps\", []):\n gw_req = _declared_gw_for_step(\n step, idx.generations_by_id, idx.gw_resp_by_resp_id, idx.gw_req_by_id)\n if gw_req is None:\n continue\n gw_id = gw_req[\"gatewayRequestId__c\"]\n if gw_id in declared_gw_ids:\n # Collision: second+ step reaches a GW already claimed.\n step_id_to_gw_id[step[\"ssot__Id__c\"]] = None\n continue\n declared_gw_ids.add(gw_id)\n declared_steps_with_gw.add(step[\"ssot__Id__c\"])\n step_id_to_gw_id[step[\"ssot__Id__c\"]] = gw_id\n return BindingResults(\n declared_gw_ids=declared_gw_ids,\n declared_steps_with_gw=frozenset(declared_steps_with_gw),\n step_id_to_gw_id=step_id_to_gw_id,\n declared_collisions=sum(1 for v in step_id_to_gw_id.values() if v is None),\n )\n\n\ndef _build_step_view(step: dict, idx: Indexes, dispatch: PolymorphicSplits,\n step_id_to_gw_id: Dict[str, Optional[str]],\n build_gw) -> dict:\n \"\"\"Emit one Step view, including its Generation and (if declared) GatewayRequest.\n\n `build_gw` is a `functools.partial` pre-bound with `idx`/`dispatch`\n (see `assemble()`); call sites only supply `gw_req`, `binding_method`,\n and optionally `bound_step_id`.\n \"\"\"\n sid_step = step[\"ssot__Id__c\"]\n gen_id = step.get(\"ssot__GenerationId__c\")\n gen = idx.generations_by_id.get(gen_id) if gen_id not in _NOT_SET else None\n generation_view = _build_generation_view(\n gen, idx.quality_by_parent, dispatch.categories_by_generation,\n dispatch.categories_by_quality, idx.feedback_by_gen,\n idx.feedback_details_by_parent, dispatch.gw_records_by_feedback,\n ) if gen is not None else None\n\n gw_id = step_id_to_gw_id.get(sid_step)\n gw_view = None\n collision_flag = gw_id is None and sid_step in step_id_to_gw_id\n if gw_id is not None:\n gw_view = build_gw(idx.gw_req_by_id[gw_id], \"declared\")\n\n # Mirror the bound gateway_request's model identifier onto the step\n # itself so renderers can show \"LLM_STEP \u003cname> · \u003cmodel>\" without\n # dereferencing the nested gateway view. The mirror is None when no\n # gateway_request is bound (declared chain didn't reach, or the STDM\n # exporter dropped writes — see the `gateway_requests_dropped_by_stdm`\n # session_shape).\n step_model_name = gw_view.get(\"model\") if gw_view else None\n\n step_view: Dict[str, Any] = {\n \"id\": sid_step,\n \"type\": step.get(\"ssot__AiAgentInteractionStepType__c\"),\n \"name\": step.get(\"ssot__Name__c\"),\n \"start_ts\": step.get(\"ssot__StartTimestamp__c\"),\n \"end_ts\": step.get(\"ssot__EndTimestamp__c\"),\n \"error_text\": _clean(step.get(\"ssot__ErrorMessageText__c\")),\n \"model_name\": step_model_name,\n \"generation\": generation_view,\n \"gateway_request\": gw_view,\n }\n if collision_flag:\n step_view[\"gateway_request_collision\"] = True\n return step_view\n\n\ndef _build_message_view(m: dict, participant_role_by_id: Dict[str, str]) -> dict:\n mtype = m.get(\"ssot__AiAgentInteractionMessageType__c\")\n pid = m.get(\"ssot__AiAgentSessionParticipantId__c\")\n role = participant_role_by_id.get(pid)\n if role is None:\n role = \"USER\" if mtype == \"Input\" else \"AGENT\" if mtype == \"Output\" else None\n return {\n \"message_id\": m.get(\"ssot__Id__c\"),\n \"type\": mtype,\n \"role\": role,\n \"participant_id\": pid,\n \"text\": m.get(\"ssot__ContentText__c\"),\n \"content_type\": m.get(\"ssot__AiAgentInteractionMsgContentType__c\"),\n \"modality\": m.get(\"Modality__c\"),\n \"ts\": m.get(\"ssot__MessageSentTimestamp__c\"),\n }\n\n\ndef _build_interaction_view(interaction: dict, rows: Dict[str, List[dict]],\n idx: Indexes, dispatch: PolymorphicSplits,\n binding: BindingResults,\n build_gw) -> Tuple[dict, BinderCtx]:\n \"\"\"Emit one Interaction view plus its BinderCtx.\n\n Returns (view, binder_ctx). Binder scratch state lives in the `BinderCtx`\n and is keyed externally by `iid`; it never touches the emitted view, so\n it can't leak into `dc._session_tree.json`. `build_gw` is the\n `functools.partial` from `assemble()`.\n \"\"\"\n iid = interaction[\"ssot__Id__c\"]\n trace_id = _extract_trace_id(interaction)\n\n steps_sorted = sorted(\n idx.steps_by_interaction.get(iid, []),\n key=lambda s: (s.get(\"ssot__StartTimestamp__c\") or \"\", s.get(\"ssot__Id__c\") or \"\"))\n step_views = [_build_step_view(s, idx, dispatch, binding.step_id_to_gw_id, build_gw)\n for s in steps_sorted]\n\n # Step windows consumed only by _ts_window_pass via the parallel BinderCtx.\n steps_with_ts: List[Tuple[dict, Optional[datetime], Optional[datetime]]] = [\n (s, _ts(s.get(\"ssot__StartTimestamp__c\")), _ts(s.get(\"ssot__EndTimestamp__c\")))\n for s in steps_sorted if _ts(s.get(\"ssot__StartTimestamp__c\")) is not None\n ]\n\n messages_sorted = sorted(\n idx.messages_by_interaction.get(iid, []),\n key=lambda r: (r.get(\"ssot__MessageSentTimestamp__c\") or \"\",\n r.get(\"ssot__Id__c\") or \"\"))\n msg_views = [_build_message_view(m, idx.participant_role_by_id)\n for m in messages_sorted]\n\n view = {\n \"id\": iid,\n \"type\": interaction.get(\"ssot__AiAgentInteractionType__c\"),\n \"topic\": _clean(interaction.get(\"ssot__TopicApiName__c\")),\n \"trace_id\": trace_id,\n \"start_ts\": interaction.get(\"ssot__StartTimestamp__c\"),\n \"end_ts\": interaction.get(\"ssot__EndTimestamp__c\"),\n \"messages\": msg_views,\n \"telemetry_spans\": [s for s in rows.get(\"telemetry_spans\", [])\n if s.get(\"ssot__TelemetryTrace__c\") == trace_id],\n \"steps\": step_views,\n \"timestamp_bound_gateway_calls\": [], # appended by _ts_window_pass\n \"tag_associations\": dispatch.tag_assoc_by_interaction.get(iid, []),\n }\n binder_ctx = BinderCtx(\n start_ts=_ts(interaction.get(\"ssot__StartTimestamp__c\")),\n end_ts=_ts(interaction.get(\"ssot__EndTimestamp__c\")),\n steps_with_ts=steps_with_ts,\n reserved_step_ids=binding.declared_steps_with_gw,\n )\n return view, binder_ctx\n\n\ndef _ts_window_pass(interactions_view: List[dict],\n binders: Dict[str, BinderCtx],\n idx: Indexes,\n binding: BindingResults,\n build_gw) -> Tuple[List[dict], dict]:\n \"\"\"Place every chain-orphan GW via timestamp-window, or into unbound[].\n\n Reads per-interaction binder state from the parallel `binders` dict keyed\n by `iv[\"id\"]` — never from the view itself. Mutates `interactions_view`\n in place only to append to `timestamp_bound_gateway_calls[]` (an emission\n field, pre-initialized to `[]` in `_build_interaction_view`).\n\n Returns (unbound_gw_calls, gw_binding_counts).\n \"\"\"\n unbound: List[dict] = []\n counts = {\n \"declared\": len(binding.declared_gw_ids),\n \"timestamp_window\": 0,\n \"unbound\": 0,\n \"declared_collisions\": binding.declared_collisions,\n }\n for gw_id, gw_req in idx.gw_req_by_id.items():\n if gw_id in binding.declared_gw_ids:\n continue\n placed = False\n for iv in interactions_view:\n bctx = binders[iv[\"id\"]]\n placement, bound_step_id = _bind_ts_window(\n gw_req,\n bctx.steps_with_ts,\n (bctx.start_ts, bctx.end_ts),\n bctx.reserved_step_ids,\n )\n if placement in (\"step\", \"interaction\"):\n iv[\"timestamp_bound_gateway_calls\"].append(\n build_gw(gw_req, \"timestamp_window\", bound_step_id=bound_step_id))\n counts[\"timestamp_window\"] += 1\n placed = True\n break\n if not placed:\n unbound.append(build_gw(gw_req, \"unbound\"))\n counts[\"unbound\"] += 1\n\n # Defense in depth: if anyone reintroduces the binder-cache-on-view\n # pattern in a future edit, this catches it before the tree ever writes.\n assert not any(k.startswith(\"_\") for iv in interactions_view for k in iv), \\\n \"binder scratch state leaked into interaction view — do not stash on the view dict\"\n\n return unbound, counts\n\n\ndef _build_moments_view(rows: Dict[str, List[dict]], dispatch: PolymorphicSplits) -> List[dict]:\n \"\"\"session.moments[] with interaction_ids[] back-refs derived from MomentInteraction.\"\"\"\n by_moment: Dict[str, List[str]] = defaultdict(list)\n for mi in rows.get(\"moment_interactions\", []):\n mid = mi.get(\"ssot__AiAgentMomentId__c\")\n iid = mi.get(\"ssot__AiAgentInteractionId__c\")\n if mid not in _NOT_SET and iid not in _NOT_SET:\n by_moment[mid].append(iid)\n\n moments_sorted = sorted(\n rows.get(\"moments\", []),\n key=lambda r: (r.get(\"ssot__StartTimestamp__c\") or \"\", r.get(\"ssot__Id__c\") or \"\"))\n return [\n {\n \"moment_id\": m.get(\"ssot__Id__c\"),\n \"agent_api_name\": _clean(m.get(\"ssot__AiAgentApiName__c\")),\n \"agent_version\": _clean(m.get(\"ssot__AiAgentVersionApiName__c\")),\n \"request_summary_text\": _clean(m.get(\"ssot__RequestSummaryText__c\")),\n \"response_summary_text\": _clean(m.get(\"ssot__ResponseSummaryText__c\")),\n \"interaction_ids\": sorted(by_moment.get(m.get(\"ssot__Id__c\"), [])),\n \"start_ts\": m.get(\"ssot__StartTimestamp__c\"),\n \"end_ts\": m.get(\"ssot__EndTimestamp__c\"),\n \"tag_associations\": dispatch.tag_assoc_by_moment.get(m.get(\"ssot__Id__c\"), []),\n }\n for m in moments_sorted\n ]\n\n\ndef _build_participants_view(rows: Dict[str, List[dict]]) -> List[dict]:\n return [\n {\n \"participant_id\": p.get(\"ssot__Id__c\"),\n \"role\": p.get(\"ssot__AiAgentSessionParticipantRole__c\"),\n \"agent_api_name\": _clean(p.get(\"ssot__AiAgentApiName__c\")),\n \"agent_version\": _clean(p.get(\"ssot__AiAgentVersionApiName__c\")),\n \"agent_type\": _clean(p.get(\"ssot__AiAgentType__c\")),\n }\n for p in sorted(\n rows.get(\"participants\", []),\n key=lambda r: (r.get(\"ssot__StartTimestamp__c\") or \"\", r.get(\"ssot__Id__c\") or \"\"))\n ]\n\n\ndef _build_counts(rows: Dict[str, List[dict]], dispatch: PolymorphicSplits,\n binding_counts: dict,\n manifest: dict, collisions: List[dict],\n parse_warnings: List[str]) -> dict:\n int_by_type: Dict[str, int] = defaultdict(int)\n step_by_type: Dict[str, int] = defaultdict(int)\n for i in rows.get(\"interactions\", []):\n int_by_type[i.get(\"ssot__AiAgentInteractionType__c\")] += 1\n for s in rows.get(\"steps\", []):\n step_by_type[s.get(\"ssot__AiAgentInteractionStepType__c\")] += 1\n\n return {\n \"interactions_total\": len(rows.get(\"interactions\", [])),\n \"interactions_turn\": int_by_type.get(\"TURN\", 0),\n \"interactions_session_end\": int_by_type.get(\"SESSION_END\", 0),\n \"steps_total\": len(rows.get(\"steps\", [])),\n \"steps_by_type\": {\n k: step_by_type.get(k, 0) for k in\n (\"LLM_STEP\", \"ACTION_STEP\", \"TOPIC_STEP\", \"TRUST_GUARDRAILS_STEP\", \"SESSION_END\")\n },\n \"generations\": len(rows.get(\"generations\", [])),\n \"gateway_requests\": len(rows.get(\"gateway_requests\", [])),\n \"gateway_responses\": len(rows.get(\"gateway_responses\", [])),\n \"gateway_metadata\": len(rows.get(\"gateway_request_metadata\", [])),\n \"gateway_llm\": len(rows.get(\"gateway_request_llm\", [])),\n \"gateway_records_grounded\": sum(len(v) for v in dispatch.gw_records_by_gw_req.values()),\n \"gateway_records_feedback\": sum(len(v) for v in dispatch.gw_records_by_feedback.values()),\n \"feedback\": len(rows.get(\"feedback\", [])),\n \"audit_chain_1to1_ok\": (\n len(rows.get(\"gateway_requests\", [])) == len(rows.get(\"gateway_responses\", []))\n ),\n \"gw_binding\": binding_counts,\n \"session_shape\": manifest.get(\"session_shape\", \"unknown\"),\n \"pk_collisions\": collisions,\n \"parse_warnings\": parse_warnings,\n }\n\n\ndef assemble(sid: str) -> Tuple[dict, Path]:\n \"\"\"Orchestrate: load → index → dispatch → bind → build views → counts → tree.\n\n Returns (tree, session_dir). The session_dir is resolved by\n ``_find_session_dir`` (via breadcrumb or glob) and passed back so\n the caller can write ``dc._session_tree.json`` next to the inputs\n without re-scanning the disk.\n \"\"\"\n manifest, rows, parse_warnings, session_dir = _load_all(sid)\n\n # Short-circuit: session row not found.\n sessions = rows.get(\"sessions\", [])\n if not sessions:\n return _minimal_tree_session_not_found(sid, manifest, parse_warnings), session_dir\n\n # Short-circuit: STDM Interaction/Step/Message DMOs haven't materialized yet\n # (gateway_requests present, interactions/steps empty). Render the gateway\n # chain directly instead of silently emitting an empty tree.\n if manifest.get(\"session_shape\") == \"interactions_not_materialized_yet\":\n return _assemble_gateway_direct(sid, rows, manifest, parse_warnings), session_dir\n\n collisions: List[dict] = []\n # Phase 1: independent bags.\n idx = _build_indexes(rows, collisions)\n dispatch = _dispatch_polymorphic(rows, idx)\n catalog = _filter_catalog(rows)\n # Phase 2: derived bag that depends on idx.\n binding = _declared_binding_pass(rows, idx)\n\n # Bind the invariant gw-view args once; call sites supply only the varying ones.\n build_gw = functools.partial(_build_gw_view, idx=idx, dispatch=dispatch)\n\n # Build per-Interaction views + parallel binder state (sorted by start_ts).\n interactions_view: List[dict] = []\n binders: Dict[str, BinderCtx] = {}\n for interaction in sorted(\n rows.get(\"interactions\", []),\n key=lambda r: (r.get(\"ssot__StartTimestamp__c\") or \"\",\n r.get(\"ssot__Id__c\") or \"\")):\n view, bctx = _build_interaction_view(interaction, rows, idx, dispatch, binding, build_gw)\n interactions_view.append(view)\n binders[view[\"id\"]] = bctx\n\n # Timestamp-window fallback; mutates interactions_view only via\n # timestamp_bound_gateway_calls[].append.\n unbound_gw_calls, gw_binding_counts = _ts_window_pass(\n interactions_view, binders, idx, binding, build_gw)\n\n session_row = sessions[0]\n session_identity = _build_session_identity(rows, manifest)\n # `org_id_15` is the canonical 15-char slice used by path helpers.\n # Prefer the manifest-stamped value (resolved in wave 1a of fetch_dc\n # from sessions[0].ssot__InternalOrganizationId__c); fall back to\n # slicing session_identity.org_id (the 18-char form from\n # gateway_requests) when the manifest is missing the field (older\n # artifacts predate the manifest stamp).\n org_id_15 = manifest.get(\"org_id_15\")\n if not org_id_15 and session_identity.get(\"org_id\"):\n org_id_15 = session_identity[\"org_id\"][:15]\n session_identity[\"org_id_15\"] = org_id_15\n\n # Top-level identity block — canonical location for the 3 segments\n # needed to name the session dir. Richer identity fields live under\n # `session.identity` as before.\n # Promote richer values from `session_identity` when the manifest carries\n # placeholders (notably ``agent_version=\"v0\"`` from the MyAgent\n # fallback in fetch_dc.py:570-597) — without this, the top-level block\n # diverges from `session.identity` in the same JSON file.\n top_identity = _reconcile_top_identity(manifest, session_identity, org_id_15)\n\n return {\n \"identity\": top_identity,\n \"session\": {\n \"id\": sid,\n \"_schema_version\": 1,\n \"org\": {\n \"alias\": manifest.get(\"org_alias\"),\n \"instance_url\": manifest.get(\"instance_url\"),\n },\n \"identity\": session_identity,\n \"start_ts\": session_row.get(\"ssot__StartTimestamp__c\"),\n \"end_ts\": session_row.get(\"ssot__EndTimestamp__c\"),\n \"end_type\": _resolve_end_type(session_row, rows),\n \"channel\": _harvest_str(session_row.get(\"ssot__AiAgentChannelType__c\")),\n \"participants\": _build_participants_view(rows),\n \"moments\": _build_moments_view(rows, dispatch),\n \"interactions\": interactions_view,\n \"session_tag_associations\": dispatch.tag_assoc_session,\n \"unbound_gateway_calls\": unbound_gw_calls,\n \"counts\": _build_counts(rows, dispatch, gw_binding_counts,\n manifest, collisions, parse_warnings),\n },\n \"catalog\": {\n \"agents_observed\": catalog.agents_observed,\n \"tag_definitions\": catalog.tag_definitions,\n \"tag_definition_associations\": catalog.tag_definition_associations,\n \"tags\": catalog.tags,\n },\n \"_doc\": (\n \"Assembled from \"\n \"DATA_ROOT/\u003corg>/\u003cagent>__\u003cver>/\u003csid>/dc.*.json. \"\n \"See dc._session_manifest.json for per-query counts and empty reasons. \"\n \"Contract: references/dc_pipeline_contract.md.\"\n ),\n }, session_dir\n\n\ndef _build_generation_view(gen: dict,\n quality_by_parent: Dict[str, List[dict]],\n categories_by_generation: Dict[str, List[dict]],\n categories_by_quality: Dict[str, List[dict]],\n feedback_by_gen: Dict[str, List[dict]],\n feedback_details_by_parent: Dict[str, List[dict]],\n gw_records_by_feedback: Dict[str, List[dict]]) -> dict:\n gen_id = gen[\"generationId__c\"]\n # Quality rows with their TOXICITY sub-categories nested.\n quality_rows = []\n for q in quality_by_parent.get(gen_id, []):\n q_view = dict(q)\n q_view[\"_toxicity_subcategories\"] = categories_by_quality.get(q.get(\"id__c\"), [])\n quality_rows.append(q_view)\n\n # Feedback rows with details + feedback-attachment records nested.\n feedback_rows = []\n for fb in feedback_by_gen.get(gen_id, []):\n fid = fb.get(\"feedbackId__c\")\n feedback_rows.append({\n \"feedback_id\": fid,\n \"feedback\": fb.get(\"feedback__c\"),\n \"action\": fb.get(\"action__c\"),\n \"details\": feedback_details_by_parent.get(fid, []),\n \"records\": gw_records_by_feedback.get(fid, []),\n })\n\n return {\n \"generation_id\": gen_id,\n \"response_id\": _clean(gen.get(\"generationResponseId__c\")),\n \"response_text\": gen.get(\"responseText__c\"),\n \"masked_response_text\": gen.get(\"maskedResponseText__c\"),\n \"response_parameters\": gen.get(\"responseParameters__c\"),\n \"feature\": _clean(gen.get(\"feature__c\")),\n \"quality\": quality_rows,\n \"categories\": categories_by_generation.get(gen_id, []),\n \"feedback\": feedback_rows,\n }\n\n\n_STDM_LAG_NOTE = (\n \"Interaction/Step/Message DMOs materialize on a separate cadence from \"\n \"Gateway DMOs. For fresh sessions this view reflects the gateway chain \"\n \"directly. Re-run in 24–72 hours for the full hierarchical trace.\"\n)\n\n\ndef _assemble_gateway_direct(sid: str, rows: Dict[str, List[dict]],\n manifest: dict,\n parse_warnings: List[str]) -> dict:\n \"\"\"Build a gateway-chain-only tree for sessions whose STDM hierarchy hasn't materialized.\n\n Mirrors ``_minimal_tree_session_not_found`` in shape, but populates a\n ``session.gateway_chain[]`` harvested directly from ``gateway_requests``\n joined to ``gateway_request_tags`` / ``gateway_request_metadata`` /\n ``gateway_responses``. ``session.interactions`` is an explicit empty list\n so consumers that walk it simply no-op.\n\n The sentinel ``_source = \"gateway_direct\"`` is consumed by\n ``render_dc._render_gateway_direct``.\n \"\"\"\n sessions = rows.get(\"sessions\", [])\n session_row = sessions[0] if sessions else {}\n\n # Reuse the canonical identity harvester — same tag-alias fallbacks,\n # same normalization — and apply the same org_id_15 fallback as the\n # happy path so the top-level identity block is consistent across shapes.\n session_identity = _build_session_identity(rows, manifest)\n org_id_15 = manifest.get(\"org_id_15\")\n if not org_id_15 and session_identity.get(\"org_id\"):\n org_id_15 = session_identity[\"org_id\"][:15]\n session_identity[\"org_id_15\"] = org_id_15\n\n # Same placeholder-promotion policy as the happy path — see\n # `_reconcile_top_identity` for why we don't trust the manifest blindly.\n top_identity = _reconcile_top_identity(manifest, session_identity, org_id_15)\n\n # Group the child DMOs by parent once so the per-request loop stays O(n).\n tags_by_req: Dict[str, List[dict]] = _groupby(\n rows.get(\"gateway_request_tags\", []), \"parent__c\")\n md_by_req: Dict[str, List[dict]] = _groupby(\n rows.get(\"gateway_request_metadata\", []), \"parent__c\")\n resp_by_req: Dict[str, List[dict]] = _groupby(\n rows.get(\"gateway_responses\", []), \"generationRequestId__c\")\n\n # Deterministic order: sort by (timestamp__c, gatewayRequestId__c) so\n # repeat runs on the same inputs produce byte-identical output.\n gw_sorted = sorted(\n rows.get(\"gateway_requests\", []),\n key=lambda r: (r.get(\"timestamp__c\") or \"\",\n r.get(\"gatewayRequestId__c\") or \"\"),\n )\n\n gateway_chain: List[dict] = []\n for gw in gw_sorted:\n gw_id = gw.get(\"gatewayRequestId__c\")\n # sf__Id (platform row id) isn't harvested by fetch_dc; the logical\n # PK is gatewayRequestId__c. Keep both keys on the view so readers\n # can lift either without re-joining.\n sf_id = gw.get(\"sf__Id\")\n\n # timestamp: prefer the columns named in the change-request\n # (requestTimeStamp__c, createdAt__c), then fall back to the\n # documented column (timestamp__c in references/dc_dmo_fields.md).\n timestamp = (\n _clean(gw.get(\"requestTimeStamp__c\"))\n or _clean(gw.get(\"createdAt__c\"))\n or _clean(gw.get(\"timestamp__c\"))\n )\n\n # Tag-driven fields. Use _harvest_str for html-unescape + quote-strip —\n # same normalizer _build_session_identity uses on tag values.\n tag_first: Dict[str, Optional[str]] = {}\n for t in sorted(\n tags_by_req.get(gw_id, []),\n key=lambda r: (r.get(\"tag__c\") or \"\",\n r.get(\"tagValue__c\") or \"\")):\n k = t.get(\"tag__c\")\n if not k or k in tag_first:\n continue\n tag_first[k] = _harvest_str(t.get(\"tagValue__c\"))\n\n # Prompt-template name: prefer the direct column on the request;\n # fall back to the tag alias used by older agent builds.\n prompt_template_dev_name = (\n _clean(gw.get(\"promptTemplateDevName__c\"))\n or tag_first.get(\"prompt_template_dev_name\")\n )\n # `feature` likewise prefers the typed column, falls back to tag.\n feature = _clean(gw.get(\"feature__c\")) or tag_first.get(\"feature\")\n\n # Response side — take the first by response timestamp. 1:1 invariant\n # holds on every live session we've observed, but defend against the\n # in-flight-call edge case by sorting deterministically.\n responses_sorted = sorted(\n resp_by_req.get(gw_id, []),\n key=lambda r: (r.get(\"timestamp__c\") or \"\",\n r.get(\"generationResponseId__c\") or \"\"),\n )\n response_view: Optional[dict] = None\n if responses_sorted:\n resp = responses_sorted[0]\n response_view = {\n \"timestamp\": _clean(resp.get(\"timestamp__c\")),\n \"finish_reason\": _parse_finish_reason_params(resp.get(\"parameters__c\")),\n }\n\n gateway_chain.append({\n \"gateway_request_id\": gw_id,\n \"sf_id\": sf_id,\n \"timestamp\": timestamp,\n \"model\": _clean(gw.get(\"model__c\")),\n \"provider\": _clean(gw.get(\"provider__c\")),\n \"prompt_template_dev_name\": prompt_template_dev_name,\n \"feature\": feature,\n \"prompt_tokens\": gw.get(\"promptTokens__c\"),\n \"completion_tokens\": gw.get(\"completionTokens__c\"),\n \"total_tokens\": gw.get(\"totalTokens__c\"),\n # Raw prompt is authoritative on disk (dc.gateway_requests.json);\n # the 64 KB display cap lives in the render layer, not here.\n \"prompt_text\": gw.get(\"prompt__c\"),\n \"metadata\": md_by_req.get(gw_id, []),\n \"tags\": tags_by_req.get(gw_id, []),\n \"response\": response_view,\n })\n\n return {\n \"identity\": top_identity,\n \"_source\": \"gateway_direct\",\n \"session\": {\n \"id\": sid,\n \"_schema_version\": 1,\n \"org\": {\n \"alias\": manifest.get(\"org_alias\"),\n \"instance_url\": manifest.get(\"instance_url\"),\n },\n \"identity\": session_identity,\n \"start_ts\": session_row.get(\"ssot__StartTimestamp__c\"),\n \"end_ts\": session_row.get(\"ssot__EndTimestamp__c\"),\n \"end_type\": _resolve_end_type(session_row, rows),\n \"channel\": _harvest_str(session_row.get(\"ssot__AiAgentChannelType__c\")),\n \"participants\": _build_participants_view(rows),\n # Explicit empty list — downstream consumers walk this and must\n # no-op safely when STDM hasn't materialized yet.\n \"interactions\": [],\n \"gateway_chain\": gateway_chain,\n \"_stdm_lag_note\": _STDM_LAG_NOTE,\n \"counts\": {\n \"session_shape\": manifest.get(\"session_shape\",\n \"interactions_not_materialized_yet\"),\n \"gateway_requests\": len(rows.get(\"gateway_requests\", [])),\n \"gateway_responses\": len(rows.get(\"gateway_responses\", [])),\n \"gateway_metadata\": len(rows.get(\"gateway_request_metadata\", [])),\n \"gateway_tags\": len(rows.get(\"gateway_request_tags\", [])),\n \"interactions_total\": 0,\n \"steps_total\": 0,\n \"parse_warnings\": parse_warnings,\n },\n },\n \"_doc\": (\n f\"Session {sid}: STDM Interaction/Step/Message DMOs have not \"\n \"materialized yet (they lag Gateway DMOs by hours to days). \"\n \"Tree carries the gateway chain harvested directly from \"\n \"GenAIGatewayRequest + related audit DMOs. Re-run fetch_dc.py \"\n \"in 24–72h once the STDM hierarchy has caught up for the full \"\n \"hierarchical trace.\"\n ),\n }\n\n\ndef _parse_finish_reason_params(parameters: Optional[str]) -> Optional[str]:\n \"\"\"Lift finish_reason out of GatewayResponse.parameters__c.\n\n The field arrives HTML-escaped and the finish_reason value itself is\n often wrapped in escaped quotes (e.g. `\\\\\"stop\\\\\"`). Mirrors\n ``render_dc._parse_finish_reason`` but against the gateway-response\n parameters column rather than Generation.responseParameters__c.\n \"\"\"\n if not parameters:\n return None\n try:\n decoded = html.unescape(parameters)\n parsed = json.loads(decoded)\n except (ValueError, TypeError):\n return None\n raw = parsed.get(\"finish_reason\") if isinstance(parsed, dict) else None\n if not isinstance(raw, str):\n return None\n return raw.replace(\"\\\\\", \"\").strip('\"').strip() or None\n\n\ndef _minimal_tree_session_not_found(sid: str, manifest: dict,\n parse_warnings: List[str]) -> dict:\n # Note: deliberately does NOT include `session.identity` — harvest\n # sources (gateway_requests, gateway_request_tags, sessions[0],\n # participants) are all empty on this path. Renderer's minimal-tree\n # branch must handle absent identity. DOES include _schema_version so\n # the renderer's version check doesn't warn on every not-found session.\n #\n # Top-level `identity` still carries the manifest-stamped (org, agent,\n # version) when available — the session dir already exists under that\n # identity or the manifest couldn't have been read. Breadcrumb readers\n # depend on this block being present on EVERY tree, not just\n # full-populated ones.\n return {\n \"identity\": {\n \"org_id_15\": manifest.get(\"org_id_15\"),\n \"agent_api_name\": manifest.get(\"agent_api_name\"),\n \"agent_version\": manifest.get(\"agent_version\"),\n },\n \"session\": {\n \"id\": sid,\n \"_schema_version\": 1,\n \"org\": {\n \"alias\": manifest.get(\"org_alias\"),\n \"instance_url\": manifest.get(\"instance_url\"),\n },\n \"counts\": {\n \"session_shape\": manifest.get(\"session_shape\", \"session_not_found\"),\n \"parse_warnings\": parse_warnings,\n },\n },\n \"_doc\": (\n f\"Session {sid} did not resolve in Data Cloud \"\n \"(sessions.json returned 0 rows). Check the session id, or wait for \"\n \"STDM materialization. No interactions, catalog, or audit rows available.\"\n ),\n }\n\n\n# ---- public entry points --------------------------------------------------\n\ndef main_for_session(sid: str) -> int:\n \"\"\"Called by fetch_dc.py's auto-run hook and by `--session` CLI.\n\n Writes ``dc._session_tree.json`` into the session dir located by\n ``_find_session_dir`` (via breadcrumb / glob) — the caller does not\n need to know ``(org, agent, version)`` to invoke the assembler.\n \"\"\"\n tree, session_dir = assemble(sid)\n tree_path = session_dir / \"dc._session_tree.json\"\n tree_path.write_text(json.dumps(tree, indent=2, sort_keys=True, default=str) + \"\\n\")\n print(f\"assemble_dc: wrote {tree_path}\", file=sys.stderr)\n return 0\n\n\ndef main() -> int:\n ap = argparse.ArgumentParser(\n prog=\"assemble_dc.py\",\n description=\"Assemble dc._session_tree.json for one session.\",\n )\n ap.add_argument(\"--session\", required=True,\n help=\"AI-agent session UUID or MessagingSession id (0Mw...). \"\n \"Messaging ids are resolved from disk \"\n \"(DATA_ROOT/*/dc.sessions.json); run fetch_dc.py first \"\n \"if the session hasn't been fetched yet.\")\n # Runtime-agnostic path overrides; default to ~/.vibe/...\n from _shared.cli_override import add_cli_flags, apply_overrides\n add_cli_flags(ap)\n args = ap.parse_args()\n apply_overrides(args, caller_globals=globals())\n from resolve_session import resolve_disk_or_live\n sid = resolve_disk_or_live(args.session)\n return main_for_session(sid)\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":71285,"content_sha256":"2f4dd73dea2d2a3f4b188f7046992c3fbce5d675fb4db005823f84763b857134"},{"filename":"scripts/config.py","content":"\"\"\"Shared constants for investigating-agentforce-d360.\n\nValues that don't vary per environment or user live here. If a value\nneeds to be overridable at runtime in the future, swap this module for\na loader — callers won't notice.\n\nPath layout::\n\n ~/.vibe/data/investigating-agentforce-d360/\n └── \u003corg_id_15>/\n └── \u003cagent_api_name>__\u003cagent_version>/\n └── \u003csession_id>/ ← session artifacts\n\n``DATA_ROOT`` is re-exported from the canonical helper in\n``scripts/_shared/paths.py`` (sibling module, inlined at the skill\nboundary). All session-dir composition MUST go through\n``paths.session_dir(...)`` — never compose ``DATA_ROOT / \u003csid>``\ndirectly, because that bypasses the 4-segment regex validation.\n\"\"\"\nfrom __future__ import annotations\n\nimport sys\nfrom pathlib import Path\n\n# Data Cloud Query API — the instance_url prefix is resolved at runtime\n# via `sf org display --target-org \u003calias> --json` (Claude does that,\n# not python). Python only owns the path.\nDC_API_PATH = \"/services/data/v66.0/ssot/query\"\n\n\n# -----------------------------------------------------------------------------\n# Shared path helpers — sibling _shared/ package\n# -----------------------------------------------------------------------------\n\n_SCRIPTS_DIR = Path(__file__).resolve().parent\nif str(_SCRIPTS_DIR) not in sys.path:\n sys.path.insert(0, str(_SCRIPTS_DIR))\n\nfrom _shared import paths # type: ignore # noqa: E402,F401 — re-export\nfrom _shared import sql # type: ignore # noqa: E402,F401 — re-export\n\n# One persistent tree per session. Writes land directly here — no\n# ephemeral /tmp staging. Re-running the same session id finds prior\n# results, which later enables per-artifact caching.\nDATA_ROOT = paths.DATA_ROOT\n","content_type":"text/x-python; charset=utf-8","language":"python","size":1792,"content_sha256":"618eba754c330ac8a91a19257098fa145dabfdb6b9d4838e34c04b0b8c35207c"},{"filename":"scripts/dc.py","content":"\"\"\"Data Cloud queries for investigating-agentforce-d360.\n\nTwo responsibilities:\n\n1. **Templates.** `load_sql(name, **params)` reads `assets/dc/*.sql` and\n substitutes `{{PLACEHOLDER}}` values. `parse(response)` turns a DC\n query response into a list of row dicts. Neither knows specific\n column names — those live in the .sql files.\n\n2. **Transport.** `resolve_org(alias)` shells out to `sf org display`\n for the instance URL and `sf org auth show-access-token` for the\n access token. `post(sql, instance_url, token)`\n POSTs the SQL to the Data Cloud Query API and returns parsed rows.\n Errors route through `DCQueryError` with the full response body + SQL\n context so callers can surface a useful message.\n `default_target_org()` reads the sf CLI default target org for entry\n points invoked without `--org`. Both it and `resolve_org` run the\n `show-access-token` capability preflight as their first sf call, so on\n either path a too-old CLI fails with the upgrade message first.\n\nPersistence lives in `storage.save`.\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport re\nimport subprocess\nimport sys\nimport urllib.error\nimport urllib.request\nfrom pathlib import Path\n\nfrom config import DC_API_PATH\n\nSQL_DIR = Path(__file__).parent.parent / \"assets\" / \"dc\"\n\n\nclass DCQueryError(RuntimeError):\n \"\"\"Data Cloud query failed. Carries full response body + SQL context.\"\"\"\n\n\n# ---- templates -------------------------------------------------------------\n\ndef load_sql(name: str, **params: str) -> str:\n \"\"\"Read assets/dc/\u003cname>.sql and substitute {{PARAM}} placeholders.\n\n Add a new query by dropping a `.sql` file into `assets/dc/` — no\n python edits needed. Callers pass whichever placeholders that\n template defines.\n \"\"\"\n sql = (SQL_DIR / f\"{name}.sql\").read_text()\n for key, value in params.items():\n sql = sql.replace(f\"{{{{{key}}}}}\", value)\n return sql.strip()\n\n\ndef parse(response: dict | None) -> list[dict]:\n \"\"\"Data Cloud response → list of row dicts. No field knowledge.\n\n Callers pick whatever columns they need by ssot__* name. Empty list\n means \"no response\" or \"zero rows\" — callers decide whether that's\n a partial state worth flagging.\n \"\"\"\n if not response:\n return []\n return response.get(\"data\") or []\n\n\n# ---- transport -------------------------------------------------------------\n\n_REDACTION_MARKER_FRAGMENT = \"show-access-token\"\n\"\"\"Substring identifying the sf CLI v2 redaction placeholder. The full\nliteral is `\"[REDACTED] Use 'sf org auth show-access-token' to view\"`,\nbut we match on the embedded subcommand name because the surrounding\nwording has shifted across sf CLI builds while the subcommand has been\nstable since forcedotcom/cli#3560 landed.\"\"\"\n\n\n# Token-redaction patterns, vendored from agentforce-architecture-analyze's\n# rest_client (this skill is self-contained and must not import across\n# skills). sf CLI failure output can echo the access token — e.g. a\n# `show-access-token --json` run that exits non-zero with the payload on\n# stdout — so every stderr/stdout string is scrubbed before it reaches a\n# SystemExit message. Deliberately permissive: over-redact, never leak.\n_AUTH_HEADER_RE = re.compile(r\"(Authorization\\s*:\\s*Bearer\\s+)\\S+\", re.IGNORECASE) # @rule-suppress starter-sec-002 — re.compile, not exec/eval\n_BEARER_RE = re.compile(r\"(\\bBearer\\s+)[^\\s\\\"']+\", re.IGNORECASE) # @rule-suppress starter-sec-002 — re.compile, not exec/eval\n_ACCESS_TOKEN_QS_RE = re.compile(r\"(access[_]?token\\s*=\\s*)[^&\\s\\\"']+\", re.IGNORECASE) # @rule-suppress starter-sec-002 — re.compile, not exec/eval\n_ACCESS_TOKEN_JSON_RE = re.compile(r\"(\\\"access[_]?token\\\"\\s*:\\s*\\\")[^\\\"]*\", re.IGNORECASE) # @rule-suppress starter-sec-002 — re.compile, not exec/eval\n\n\ndef _redact_text(text: str | None) -> str:\n \"\"\"Scrub bearer tokens and accessToken values from sf CLI output.\"\"\"\n if not text:\n return \"\"\n text = _AUTH_HEADER_RE.sub(r\"\\1\u003credacted>\", text)\n text = _BEARER_RE.sub(r\"\\1\u003credacted>\", text)\n text = _ACCESS_TOKEN_QS_RE.sub(r\"\\1\u003credacted>\", text)\n text = _ACCESS_TOKEN_JSON_RE.sub(r\"\\1\u003credacted>\", text)\n return text\n\n\ndef _run_sf_json(argv: list[str]) -> dict:\n \"\"\"Shell out + parse JSON. SystemExits on FileNotFoundError /\n CalledProcessError so callers get a clean message instead of a stack.\n Failure output is redacted before it is surfaced.\"\"\"\n try:\n r = subprocess.run(argv, capture_output=True, text=True, check=True)\n except FileNotFoundError:\n raise SystemExit(\"sf CLI not found on PATH — install Salesforce CLI first\")\n except subprocess.CalledProcessError as e:\n detail = _redact_text((e.stderr or \"\").strip() or (e.stdout or \"\").strip())\n raise SystemExit(f\"{' '.join(argv[:4])}... failed:\\n{detail}\") from None\n return json.loads(r.stdout)\n\n\n_show_access_token_capability_ok = False\n\"\"\"Set once the preflight succeeds; the check then never reruns in this\nprocess. Failures are NOT cached, so a fixed PATH/upgrade is picked up on\nthe next call.\"\"\"\n\n_SHOW_ACCESS_TOKEN_HELP_SIGNATURE_RE = re.compile(\n r\"\\$\\s*sf\\s+org\\s+auth\\s+show-access-token\\b\"\n)\n\"\"\"USAGE-line form that real `--help` output always contains\n(`$ sf org auth show-access-token -o \u003cvalue> ...`). The bare command name is\nnot enough: unknown-command notices echo it too\n(`Command org:auth:show-access-token not found`,\n`show-access-token is not a sf command`). A zero-exit probe whose output\nlacks the USAGE form is treated as a failed check.\"\"\"\n\n_ANSI_ESCAPE_RE = re.compile(r\"\\x1b\\[[0-9;?]*[ -/]*[@-~]\")\n\n\ndef _assert_show_access_token_capability() -> None:\n \"\"\"Fail fast if the installed sf CLI lacks `sf org auth show-access-token`.\n\n That command is the only supported token-retrieval path\n (forcedotcom/cli#3560). Probing `--help` needs no org auth and turns a\n confusing downstream failure into an actionable upgrade message.\n `default_target_org` and `resolve_org` each call this first, ahead of\n every other sf call they make. A successful probe is cached for the\n process lifetime, so the second call costs nothing.\n\n Exit status alone is not trusted: a CLI build that answers an unknown\n command with exit 0 (e.g. printing top-level help or a \"not found\"\n notice) would otherwise pass. The probe therefore also requires the\n command's USAGE line (`$ sf org auth show-access-token`) in the\n ANSI-stripped stdout+stderr; if it is absent the check fails and is not\n cached.\n \"\"\"\n global _show_access_token_capability_ok\n if _show_access_token_capability_ok:\n return\n try:\n proc = subprocess.run(\n [\"sf\", \"org\", \"auth\", \"show-access-token\", \"--help\"],\n capture_output=True,\n text=True,\n timeout=30,\n check=True,\n )\n except FileNotFoundError as exc:\n raise SystemExit(\n \"sf CLI not found on PATH — install Salesforce CLI first\"\n ) from exc\n except subprocess.CalledProcessError as exc:\n detail = (\n _redact_text((exc.stderr or \"\").strip() or (exc.stdout or \"\").strip())\n or \"unknown error\"\n )\n raise SystemExit(\n \"sf CLI is missing required command \"\n \"'sf org auth show-access-token'. Upgrade/reinstall Salesforce CLI \"\n \"to a build that provides this command, then retry.\\n\"\n f\"Preflight detail: {detail}\"\n ) from exc\n except subprocess.TimeoutExpired as exc:\n raise SystemExit(\n \"sf CLI capability preflight for 'sf org auth show-access-token' \"\n \"timed out after 30s\"\n ) from exc\n help_text = _ANSI_ESCAPE_RE.sub(\n \"\", f\"{proc.stdout or ''}\\n{proc.stderr or ''}\",\n )\n if not _SHOW_ACCESS_TOKEN_HELP_SIGNATURE_RE.search(help_text):\n # Zero exit without the command's help signature — the CLI did not\n # recognise the command. Fail (uncached) like a non-zero exit.\n # Redact before truncating so a token can never be split past the regex.\n detail = (\n _redact_text(help_text.strip())[:500]\n or \"help output lacked the command signature\"\n )\n raise SystemExit(\n \"sf CLI is missing required command \"\n \"'sf org auth show-access-token'. Upgrade/reinstall Salesforce CLI \"\n \"to a build that provides this command, then retry.\\n\"\n f\"Preflight detail: {detail}\"\n )\n _show_access_token_capability_ok = True\n\n\nNO_DEFAULT_ORG_MSG = (\n \"no --org given and no default target org set — run \"\n \"`sf config set target-org \u003calias>` or pass --org \u003calias>\"\n)\n\n\ndef default_target_org() -> str:\n \"\"\"Return the sf CLI default target org alias/username.\n\n Used by entry points when ``--org`` is omitted (\"my org\" means the CLI\n default). Reads ``.result[0].value`` from\n ``sf config get target-org --json``. Raises SystemExit with a clear,\n redacted message when the CLI is missing, fails, times out, returns\n non-JSON, or has no default set.\n\n The show-access-token capability preflight runs first, before\n ``sf config get``. A CLI too old to fetch a token then fails with the\n upgrade/reinstall message, not a misleading \"no default org\" message.\n The preflight is cached per process, so the later ``resolve_org`` call\n does not probe again.\n \"\"\"\n _assert_show_access_token_capability()\n argv = [\"sf\", \"config\", \"get\", \"target-org\", \"--json\"]\n try:\n proc = subprocess.run(\n argv, capture_output=True, text=True, timeout=30, check=True,\n )\n except FileNotFoundError:\n raise SystemExit(\n \"sf CLI not found on PATH — install Salesforce CLI first\"\n ) from None\n except subprocess.CalledProcessError as exc:\n detail = (\n _redact_text((exc.stderr or \"\").strip() or (exc.stdout or \"\").strip())\n or \"unknown error\"\n )\n raise SystemExit(\n f\"{NO_DEFAULT_ORG_MSG}\\n`sf config get target-org` failed: {detail}\"\n ) from None\n except subprocess.TimeoutExpired:\n raise SystemExit(\n f\"{NO_DEFAULT_ORG_MSG}\\n`sf config get target-org` timed out after 30s\"\n ) from None\n try:\n payload = json.loads(proc.stdout or \"\")\n except json.JSONDecodeError:\n raise SystemExit(\n f\"{NO_DEFAULT_ORG_MSG}\\n`sf config get target-org --json` returned \"\n f\"non-JSON output: {_redact_text(proc.stdout.strip())[:200]}\"\n ) from None\n result = payload.get(\"result\") if isinstance(payload, dict) else None\n first = result[0] if isinstance(result, list) and result else {}\n value = first.get(\"value\") if isinstance(first, dict) else None\n if not isinstance(value, str) or not value.strip():\n raise SystemExit(NO_DEFAULT_ORG_MSG)\n return value.strip()\n\n\ndef resolve_org(alias: str) -> tuple[str, str]:\n \"\"\"Resolve instanceUrl + accessToken for the sf org alias.\n\n Token retrieval is fail-fast (forcedotcom/cli#3560): the access token\n comes ONLY from `sf org auth show-access-token --json --no-prompt`.\n `sf org display --json --verbose` is used for non-secret metadata\n (instanceUrl); any `accessToken` it carries is ignored.\n\n Raises SystemExit on CLI failure — this is the one thing every\n downstream operation depends on, so a clean early exit is friendlier\n than a stack trace.\n \"\"\"\n # Step 1: capability preflight first, so a missing sf binary or missing\n # command yields the actionable preflight error (cached once it passes).\n _assert_show_access_token_capability()\n\n # Step 2: org_display for instanceUrl (non-secret metadata only).\n display = _run_sf_json(\n [\"sf\", \"org\", \"display\", \"--target-org\", alias, \"--json\", \"--verbose\"],\n )[\"result\"]\n instance_url = display.get(\"instanceUrl\") or \"\"\n if not instance_url:\n raise SystemExit(\n f\"sf org display returned no instanceUrl for alias {alias!r}\"\n )\n\n # Step 3: dedicated command is the only token source.\n try:\n token_payload = _run_sf_json(\n [\"sf\", \"org\", \"auth\", \"show-access-token\",\n \"--target-org\", alias, \"--json\", \"--no-prompt\"],\n )\n except (SystemExit, json.JSONDecodeError) as exc:\n raise SystemExit(\n f\"could not retrieve access token for alias {alias!r} via \"\n \"sf org auth show-access-token; ensure the org is authenticated \"\n f\"(sf org login web --alias {alias}).\\nDetail: {exc}\"\n ) from exc\n\n token_result = token_payload.get(\"result\") or {}\n access_token = token_result.get(\"accessToken\") or \"\"\n\n if not access_token or _REDACTION_MARKER_FRAGMENT in access_token:\n raise SystemExit(\n f\"could not retrieve a usable access token for alias {alias!r} \"\n \"via sf org auth show-access-token. Ensure sf CLI is logged in \"\n \"for this org and the command returned a real token.\"\n )\n\n return instance_url, access_token\n\n\ndef post(sql: str, instance_url: str, token: str, query_name: str = \"\") -> list[dict]:\n \"\"\"POST SQL to Data Cloud Query API, return parsed rows.\n\n `query_name` is a human label used only for error messages — pass\n the template name (e.g. \"sessions\") so DCQueryError identifies\n which query failed.\n \"\"\"\n req = urllib.request.Request(\n f\"{instance_url}{DC_API_PATH}\",\n data=json.dumps({\"sql\": sql}).encode(),\n headers={\n \"Authorization\": f\"Bearer {token}\",\n \"Content-Type\": \"application/json\",\n },\n method=\"POST\",\n )\n try:\n with urllib.request.urlopen(req, timeout=60) as resp:\n body = json.loads(resp.read())\n except urllib.error.HTTPError as e:\n err_body = e.read().decode(errors=\"replace\")\n raise DCQueryError(\n f\"query={query_name or '?'} http={e.code}\\n\"\n f\"--- response ({len(err_body)} bytes) ---\\n{err_body}\\n\"\n f\"--- sql (first 400 chars) ---\\n{sql[:400]}\"\n ) from None\n return parse(body)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":14135,"content_sha256":"4773345c867bd3d9a80b61f62e33eb5a20d1d03575a69540ab111773c383da53"},{"filename":"scripts/discover_sessions.py","content":"\"\"\"Session discovery for investigating-agentforce-d360 — Data Cloud only.\n\nLets the user find candidate sessions when they don't have a session id.\nOutput is a numbered picker table to stdout; the skill agent asks the user\nto pick one, then falls through to the existing fetch_dc.py pipeline with\nthe chosen UUID. Writes no artifacts.\n\nDC-only by design (no OTel path).\n\n## CLI\n\n python3 scripts/discover_sessions.py [--org \u003calias>] [options]\n\nOptions (all optional):\n --org \u003calias> sf CLI org alias. Default: the sf CLI default target org\n (`sf config get target-org`).\n --since \u003cexpr> \"last 2 hours\" | \"last 10\" | \"today\" | \"yesterday\"\n | \"YYYY-MM-DD\" | \"YYYY-MM-DD to YYYY-MM-DD\"\n | explicit UTC/TZ datetime (RFC 3339). Default: last 24 hours.\n --agent \u003cname> Agent API name (e.g. MyAgent). Adds participant JOIN.\n --channel \u003ctype> Builder | Messaging | Voice. \"Messaging\" maps to\n \"SCRT2 - EmbeddedMessaging\" in SQL.\n --outcome \u003ctype> USER_ENDED | ESCALATED | TRANSFERRED | TIMEOUT | NOT_SET\n --grep \u003cpattern> Substring in conversation text (user input + agent output).\n Adds interaction + message JOINs. Case-insensitive by\n default (LOWER on both sides); pass --grep-case-sensitive\n for exact match. `%` and `_` are escaped so they match\n the literal char.\n --grep-case-sensitive\n Opt-in: make --grep case-sensitive (rare; default is\n case-insensitive because users almost always type the\n quote in whatever case they remember it).\n --tz \u003cIANA> Override auto-detected local tz (e.g. America/Los_Angeles).\n Only affects calendar-day resolution (today/yesterday/\n bare-date/date-range); `last N` windows are anchored to\n UTC regardless. The chosen tz is echoed in zero-row\n output so the user can sanity-check boundaries.\n --limit \u003cN> Max sessions. Default 20.\n\nExit codes:\n 0 — ≥1 session found\n 2 — zero sessions (picker prints composed SQL so user can widen)\n other — DC query or arg-parse error\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport html\nimport os\nimport re\nimport sys\nfrom dataclasses import dataclass\nfrom datetime import datetime, timedelta, timezone\nfrom pathlib import Path\nfrom typing import Optional\nfrom zoneinfo import ZoneInfo, ZoneInfoNotFoundError\n\nsys.path.insert(0, str(Path(__file__).parent))\n\nfrom dc import DCQueryError, default_target_org, load_sql, post, resolve_org\nfrom _shared.sql import _escape_sql_literal\n\n\n# ---- channel alias ---------------------------------------------------------\n\n# Source-skill convention: users type \"Messaging\" but DC stores the full\n# SCRT2 string. Other channels pass through as-is (Builder, Voice).\n_CHANNEL_ALIAS = {\n \"Messaging\": \"SCRT2 - EmbeddedMessaging\",\n}\n\n\n# ---- escaping --------------------------------------------------------------\n#\n# `_escape_sql_literal` is imported from `_shared.sql` (single source of\n# truth per its module docstring). Do NOT re-declare locally — divergence\n# would break the documented contract that every DC-SQL caller shares one\n# escape strategy.\n\n_LIKE_ESCAPE = \"!\" # ASCII, doesn't collide with SQL syntax, no quoting ambiguity\n\ndef _escape_like_pattern(s: str) -> str:\n \"\"\"Escape SQL LIKE wildcards (`%`, `_`) and the escape char itself\n (`!`) so `--grep \"100%_off\"` matches the literal text, not \"100\n anything-underscore-off\". Quote-doubling still happens on top of this\n via _escape_sql_literal. Used with the `ESCAPE '!'` clause in the\n composed SQL.\n\n Backslash was avoided because `ESCAPE '\\\\'` in source reduces to a\n bare backslash on the wire, which some SQL parsers read as an\n unterminated string literal (\\\\' → escaped quote).\n \"\"\"\n return (\n s.replace(_LIKE_ESCAPE, _LIKE_ESCAPE * 2)\n .replace(\"%\", f\"{_LIKE_ESCAPE}%\")\n .replace(\"_\", f\"{_LIKE_ESCAPE}_\")\n )\n\n\n# ---- time parsing ----------------------------------------------------------\n\n@dataclass(frozen=True)\nclass TimeRange:\n \"\"\"Half-open UTC range [start, end). `expr` is the original user input\n so the picker header can echo back what was searched.\"\"\"\n start_utc: datetime\n end_utc: datetime\n expr: str\n tz_name: str # IANA or \"UTC\"\n\n def iso(self, dt: datetime) -> str:\n return dt.strftime(\"%Y-%m-%dT%H:%M:%S.000Z\")\n\n\ndef _autodetect_tz() -> str:\n \"\"\"IANA tz from /etc/localtime symlink (macOS / most Linux). Falls\n back to the TZ env var, then UTC.\"\"\"\n link = Path(\"/etc/localtime\")\n try:\n if link.is_symlink():\n target = os.readlink(link)\n # Typically .../zoneinfo/America/Los_Angeles\n if \"zoneinfo/\" in target:\n return target.split(\"zoneinfo/\", 1)[1]\n except OSError:\n pass\n return os.environ.get(\"TZ\") or \"UTC\"\n\n\n_RX_DATE = re.compile(r\"^\\d{4}-\\d{2}-\\d{2}$\") # @rule-suppress starter-sec-002 — re.compile, not eval/exec\n_RX_DATE_RANGE = re.compile(r\"^(\\d{4}-\\d{2}-\\d{2})\\s+to\\s+(\\d{4}-\\d{2}-\\d{2})$\") # @rule-suppress starter-sec-002 — re.compile, not eval/exec\n_RX_LAST_N = re.compile( # @rule-suppress starter-sec-002 — re.compile, not eval/exec\n r\"^last\\s+(\\d+)(?:\\s+(hour|hours|day|days|minute|minutes))?$\",\n re.IGNORECASE,\n)\n\n\ndef parse_time_expr(expr: Optional[str], tz_name: str) -> TimeRange:\n \"\"\"Parse one of the supported time expressions into a UTC half-open\n range. Default (None) is \"last 24 hours\" so every query has a sane\n time predicate — DC scans over the whole DMO without one are slow.\"\"\"\n tz_name = tz_name or _autodetect_tz()\n try:\n local_tz = ZoneInfo(tz_name)\n except ZoneInfoNotFoundError:\n raise SystemExit(f\"discover_sessions: unknown IANA timezone {tz_name!r}\")\n\n now_utc = datetime.now(timezone.utc)\n\n if expr is None:\n return TimeRange(\n start_utc=now_utc - timedelta(hours=24),\n end_utc=now_utc,\n expr=\"last 24 hours (default)\",\n tz_name=tz_name,\n )\n\n s = expr.strip()\n sl = s.lower()\n\n # \"last N\" / \"last N hours\" / \"last N days\" / \"last N minutes\"\n m = _RX_LAST_N.match(sl)\n if m:\n n = int(m.group(1))\n unit = (m.group(2) or \"hours\").lower()\n if unit in (\"hour\", \"hours\"):\n delta = timedelta(hours=n)\n elif unit in (\"day\", \"days\"):\n delta = timedelta(days=n)\n elif unit in (\"minute\", \"minutes\"):\n delta = timedelta(minutes=n)\n else:\n raise SystemExit(f\"discover_sessions: unsupported time unit {unit!r}\")\n return TimeRange(\n start_utc=now_utc - delta,\n end_utc=now_utc,\n expr=s,\n tz_name=tz_name,\n )\n\n # \"today\" / \"yesterday\" — calendar day in local tz\n if sl in (\"today\", \"yesterday\"):\n local_now = datetime.now(local_tz)\n offset_days = 0 if sl == \"today\" else 1\n start_local = local_now.replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=offset_days)\n end_local = start_local + timedelta(days=1)\n return TimeRange(\n start_utc=start_local.astimezone(timezone.utc),\n end_utc=end_local.astimezone(timezone.utc),\n expr=s,\n tz_name=tz_name,\n )\n\n # \"YYYY-MM-DD to YYYY-MM-DD\" — inclusive range in local tz\n m = _RX_DATE_RANGE.match(s)\n if m:\n start_d, end_d = m.group(1), m.group(2)\n start_local = datetime.strptime(start_d, \"%Y-%m-%d\").replace(tzinfo=local_tz)\n end_local = datetime.strptime(end_d, \"%Y-%m-%d\").replace(tzinfo=local_tz) + timedelta(days=1)\n return TimeRange(\n start_utc=start_local.astimezone(timezone.utc),\n end_utc=end_local.astimezone(timezone.utc),\n expr=s,\n tz_name=tz_name,\n )\n\n # Bare \"YYYY-MM-DD\"\n if _RX_DATE.match(s):\n start_local = datetime.strptime(s, \"%Y-%m-%d\").replace(tzinfo=local_tz)\n end_local = start_local + timedelta(days=1)\n return TimeRange(\n start_utc=start_local.astimezone(timezone.utc),\n end_utc=end_local.astimezone(timezone.utc),\n expr=s,\n tz_name=tz_name,\n )\n\n # Try RFC 3339 single datetime — caller probably meant \"since this time\"\n try:\n dt = datetime.fromisoformat(s.replace(\"Z\", \"+00:00\"))\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=local_tz)\n return TimeRange(\n start_utc=dt.astimezone(timezone.utc),\n end_utc=now_utc,\n expr=f\"since {s}\",\n tz_name=tz_name,\n )\n except ValueError:\n pass\n\n raise SystemExit(\n f\"discover_sessions: cannot parse --since {s!r}. \"\n f\"Try: `last 2 hours`, `today`, `yesterday`, `2026-04-22`, \"\n f\"`2026-04-22 to 2026-04-25`, or an ISO datetime.\"\n )\n\n\n# ---- SQL composition -------------------------------------------------------\n\ndef compose_sql(\n *,\n tr: TimeRange,\n agent: Optional[str],\n channel: Optional[str],\n outcome: Optional[str],\n grep: Optional[str],\n grep_case_sensitive: bool = False,\n limit: int,\n) -> str:\n \"\"\"Build the discovery SQL. Conditional JOINs + WHERE ANDs are\n composed from `tr` + flag args; the SQL skeleton lives at\n `assets/dc/discover_sessions.sql`.\"\"\"\n joins: list[str] = []\n wheres: list[str] = [\n f\"s.ssot__StartTimestamp__c >= '{tr.iso(tr.start_utc)}'\",\n f\"s.ssot__StartTimestamp__c \u003c '{tr.iso(tr.end_utc)}'\",\n ]\n\n if agent:\n joins.append(\n \"JOIN ssot__AiAgentSessionParticipant__dlm p \"\n \"ON s.ssot__Id__c = p.ssot__AiAgentSessionId__c\"\n )\n wheres.append(f\"p.ssot__AiAgentApiName__c = '{_escape_sql_literal(agent)}'\")\n\n if grep:\n joins.append(\n \"JOIN ssot__AiAgentInteraction__dlm i \"\n \"ON s.ssot__Id__c = i.ssot__AiAgentSessionId__c\"\n )\n joins.append(\n \"JOIN ssot__AiAgentInteractionMessage__dlm m \"\n \"ON i.ssot__Id__c = m.ssot__AiAgentInteractionId__c\"\n )\n # LIKE wildcards (%, _) and the escape char (!) are neutralized so\n # `--grep \"100%_off\"` matches the literal substring. Quote-doubling\n # is layered on top via _escape_sql_literal. The `ESCAPE '!'` clause\n # tells SQL the escape char is `!`.\n pat = _escape_sql_literal(_escape_like_pattern(grep))\n if grep_case_sensitive:\n wheres.append(\n f\"m.ssot__ContentText__c LIKE '%{pat}%' ESCAPE '{_LIKE_ESCAPE}'\"\n )\n else:\n # Lowercase both sides so the predicate is case-insensitive.\n # Default behaviour — users almost always type the quoted\n # phrase in whatever case they remember, not the case the\n # agent rendered it in.\n wheres.append(\n f\"LOWER(m.ssot__ContentText__c) LIKE LOWER('%{pat}%') \"\n f\"ESCAPE '{_LIKE_ESCAPE}'\"\n )\n\n if channel:\n ch = _CHANNEL_ALIAS.get(channel, channel)\n wheres.append(f\"s.ssot__AiAgentChannelType__c = '{_escape_sql_literal(ch)}'\")\n\n if outcome:\n wheres.append(f\"s.ssot__AiAgentSessionEndType__c = '{_escape_sql_literal(outcome)}'\")\n\n # DISTINCT when JOINs are present — a session can JOIN multiple\n # participants / messages and explode rows.\n columns = (\n \"s.ssot__Id__c, s.ssot__StartTimestamp__c, s.ssot__EndTimestamp__c, \"\n \"s.ssot__AiAgentChannelType__c, s.ssot__AiAgentSessionEndType__c\"\n )\n select_list = f\"DISTINCT {columns}\" if joins else columns\n\n return load_sql(\n \"discover_sessions\",\n SELECT_LIST=select_list,\n JOINS=\"\\n\".join(joins),\n WHERE_CLAUSE=\" AND \".join(wheres),\n LIMIT=str(limit),\n )\n\n\n# ---- agent-name enrichment -------------------------------------------------\n\n_NAME_NOT_SET = {\"\", \"NOT_SET\", None}\n\n\ndef fetch_agent_names(\n session_ids: list[str], *, instance_url: str, token: str\n) -> dict[str, str]:\n \"\"\"One follow-up query to grab agent names for discovered sessions.\n The discovery query itself drops agent name from the projection\n because adding it to SELECT forces a participant JOIN on every query\n (including the no-filter case) and DC penalizes that.\n\n Returns {session_uuid: agent_api_name}. Missing sessions → not in dict.\n\n No role filter on the SQL. Some agent shapes (e.g. MyAgent) leave\n AGENT-role rows with ``ssot__AiAgentApiName__c = 'NOT_SET'`` while USER-\n role rows correctly carry the agent's api_name. Filtering to ``role =\n 'AGENT'`` would either return no row (picker shows '—' for a session\n that clearly has an agent) or return the literal ``'NOT_SET'`` (picker\n misleads the operator). Dropping the role filter and de-duplicating in\n Python with the dominant-agent rule (``sorted(...)[0]``) recovers both\n cases. Mirrors the fallback in ``fetch_dc._resolve_identity``.\n \"\"\"\n if not session_ids:\n return {}\n quoted = \", \".join(f\"'{_escape_sql_literal(sid)}'\" for sid in session_ids)\n sql = (\n \"SELECT ssot__AiAgentSessionId__c, ssot__AiAgentApiName__c \"\n \"FROM ssot__AiAgentSessionParticipant__dlm \"\n f\"WHERE ssot__AiAgentSessionId__c IN ({quoted})\"\n )\n try:\n rows = post(sql, instance_url, token, \"discover_sessions_agent_names\")\n except DCQueryError:\n return {} # non-fatal — picker shows \"—\" for agent\n # Group all api_names per session, drop NOT_SET sentinels, dominant-agent\n # pick (lexicographic first) to match the rule used in fetch_dc.\n # Sessions where every participant row is NOT_SET fall out of the result\n # entirely — the picker will show '—', which is more honest than\n # displaying the literal 'NOT_SET'.\n by_sid: dict[str, set[str]] = {}\n for row in rows:\n sid = row.get(\"ssot__AiAgentSessionId__c\")\n name = row.get(\"ssot__AiAgentApiName__c\")\n if not sid or name in _NAME_NOT_SET:\n continue\n by_sid.setdefault(sid, set()).add(name)\n return {sid: sorted(names)[0] for sid, names in by_sid.items() if names}\n\n\n# ---- rendering -------------------------------------------------------------\n\ndef _fmt_duration(start_iso: Optional[str], end_iso: Optional[str]) -> str:\n \"\"\"Human-readable duration (e.g. \"1m 12s\", \"28s\") from two ISO timestamps.\n Returns \"—\" if start_iso is missing or unparseable. When end_iso is\n None but start_iso parses, computes elapsed-from-now and appends \"+\"\n to signal the session is still open (or end timestamp hasn't\n materialized in DC yet).\"\"\"\n if not start_iso:\n return \"—\"\n try:\n start = datetime.fromisoformat(start_iso.replace(\"Z\", \"+00:00\"))\n except ValueError:\n return \"—\"\n ongoing = False\n if not end_iso:\n end = datetime.now(timezone.utc)\n ongoing = True\n else:\n try:\n end = datetime.fromisoformat(end_iso.replace(\"Z\", \"+00:00\"))\n except ValueError:\n return \"—\"\n seconds = int((end - start).total_seconds())\n if seconds \u003c 0:\n return \"—\"\n suffix = \"+\" if ongoing else \"\"\n if seconds \u003c 60:\n return f\"{seconds}s{suffix}\"\n m, s = divmod(seconds, 60)\n if m \u003c 60:\n return (f\"{m}m {s}s\" if s else f\"{m}m\") + suffix\n h, m = divmod(m, 60)\n return f\"{h}h {m}m{suffix}\"\n\n\ndef _fmt_channel(raw: Optional[str]) -> str:\n \"\"\"Compact channel label for the picker.\"\"\"\n if not raw:\n return \"—\"\n # Reverse the Messaging alias for readability.\n if raw == \"SCRT2 - EmbeddedMessaging\":\n return \"Messaging\"\n # Custom channel labels arrive HTML-encoded from DC (e.g. \"E & O\").\n return html.unescape(raw)\n\n\ndef _fmt_start(iso: Optional[str]) -> str:\n if not iso:\n return \"—\"\n try:\n dt = datetime.fromisoformat(iso.replace(\"Z\", \"+00:00\"))\n except ValueError:\n return iso\n return dt.strftime(\"%Y-%m-%d %H:%M:%SZ\")\n\n\ndef render_picker(\n *,\n rows: list[dict],\n agent_by_sid: dict[str, str],\n org: str,\n tr: TimeRange,\n filters: dict[str, Optional[str]],\n composed_sql: str,\n) -> str:\n \"\"\"Markdown picker. Returned as a string so the caller prints once\n (single subprocess write on stdout).\"\"\"\n filter_bits = [f\"{tr.expr}\"]\n for k, v in filters.items():\n if v:\n filter_bits.append(f\"{k}={v}\")\n header_filters = \" · \".join(filter_bits)\n\n if not rows:\n # Echo the composed SQL so the user can widen the query.\n return (\n f\"No sessions matched in **{org}** ({header_filters}).\\n\\n\"\n f\"Range searched (UTC): `{tr.iso(tr.start_utc)}` → `{tr.iso(tr.end_utc)}`\\n\"\n f\"Local tz: `{tr.tz_name}`\\n\\n\"\n f\"Composed SQL:\\n```sql\\n{composed_sql}\\n```\\n\"\n f\"Try widening: larger `--since`, drop `--channel`/`--outcome`, or broaden `--grep`.\"\n )\n\n lines = [\n f\"Found **{len(rows)}** session{'s' if len(rows) != 1 else ''} in \"\n f\"**{org}** ({header_filters}):\",\n \"\",\n \"| # | UUID | Start (UTC) | Agent | Channel | Duration | Outcome |\",\n \"|---|------|-------------|-------|---------|----------|---------|\",\n ]\n for i, row in enumerate(rows, start=1):\n uuid = row.get(\"ssot__Id__c\") or \"—\"\n agent = agent_by_sid.get(uuid, \"—\")\n lines.append(\n f\"| {i} | `{uuid}` | {_fmt_start(row.get('ssot__StartTimestamp__c'))} \"\n f\"| {agent} | {_fmt_channel(row.get('ssot__AiAgentChannelType__c'))} \"\n f\"| {_fmt_duration(row.get('ssot__StartTimestamp__c'), row.get('ssot__EndTimestamp__c'))} \"\n f\"| {row.get('ssot__AiAgentSessionEndType__c') or '—'} |\"\n )\n lines.append(\"\")\n lines.append(f\"Pick a number (1–{len(rows)}) to trace, or paste a UUID.\")\n return \"\\n\".join(lines)\n\n\n# ---- CLI -------------------------------------------------------------------\n\ndef main() -> int:\n ap = argparse.ArgumentParser(\n description=\"Find candidate Agentforce sessions via Data Cloud. \"\n \"Prints a numbered picker; no artifacts written.\",\n )\n ap.add_argument(\"--org\", help=\"sf CLI org alias (the one you configured via `sf org login`). \"\n \"Default: the sf CLI default target org\")\n ap.add_argument(\"--since\", help='time range: \"last 2 hours\" | \"today\" | \"yesterday\" | '\n '\"YYYY-MM-DD\" | \"YYYY-MM-DD to YYYY-MM-DD\" | ISO datetime. '\n 'Default: last 24 hours.')\n ap.add_argument(\"--agent\", help=\"Agent API name (exact match)\")\n ap.add_argument(\"--channel\", help=\"Builder | Messaging | Voice (or full STDM string)\")\n ap.add_argument(\"--outcome\", help=\"USER_ENDED | ESCALATED | TRANSFERRED | TIMEOUT | NOT_SET\")\n ap.add_argument(\n \"--grep\",\n help=\"substring in conversation text (user + agent); \"\n \"case-insensitive by default — pass --grep-case-sensitive for exact match\",\n )\n ap.add_argument(\n \"--grep-case-sensitive\",\n action=\"store_true\",\n default=False,\n help=\"opt-in: make --grep case-sensitive (rare; default is case-insensitive)\",\n )\n ap.add_argument(\"--tz\", help=\"IANA timezone override for calendar-day resolution\")\n ap.add_argument(\"--limit\", type=int, default=20, help=\"max sessions (default 20)\")\n # Runtime-agnostic path overrides; default to ~/.vibe/...\n # discover_sessions doesn't read DATA_ROOT directly, but accepts the flags\n # for contract consistency with the other 4 d360 entry scripts.\n from _shared.cli_override import add_cli_flags, apply_overrides\n add_cli_flags(ap)\n args = ap.parse_args()\n apply_overrides(args, caller_globals=globals())\n\n if args.limit \u003c 1:\n raise SystemExit(\"discover_sessions: --limit must be >= 1\")\n\n tr = parse_time_expr(args.since, args.tz or \"\")\n sql = compose_sql(\n tr=tr,\n agent=args.agent,\n channel=args.channel,\n outcome=args.outcome,\n grep=args.grep,\n grep_case_sensitive=args.grep_case_sensitive,\n limit=args.limit,\n )\n\n org = args.org or default_target_org()\n instance_url, token = resolve_org(org)\n try:\n rows = post(sql, instance_url, token, \"discover_sessions\")\n except DCQueryError as e:\n print(f\"discover_sessions: DC query failed.\\n{e}\", file=sys.stderr)\n return 1\n\n agent_by_sid: dict[str, str] = {}\n if rows:\n sids = [r[\"ssot__Id__c\"] for r in rows if r.get(\"ssot__Id__c\")]\n agent_by_sid = fetch_agent_names(sids, instance_url=instance_url, token=token)\n\n filters = {\n \"agent\": args.agent,\n \"channel\": args.channel,\n \"outcome\": args.outcome,\n \"grep\": args.grep,\n # Only surface the case-sensitive note when --grep is in play AND\n # the user opted into exact match — the default (insensitive) is\n # the boring path and shouldn't clutter the picker header.\n \"grep_case_sensitive\": \"yes\" if (args.grep and args.grep_case_sensitive) else None,\n }\n print(render_picker(\n rows=rows,\n agent_by_sid=agent_by_sid,\n org=org,\n tr=tr,\n filters=filters,\n composed_sql=sql,\n ))\n return 0 if rows else 2\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":21593,"content_sha256":"fb31fad3837ce10ca368b2aa3968f7256dd9b2320da31f5d1cad62684336ed5c"},{"filename":"scripts/fetch_dc.py","content":"\"\"\"Fetch all 24 Data Cloud artifacts for one Agentforce session.\n\nGiven a session UUID and an `sf` org alias, this CLI drives the waterfall\nof DC queries defined in `assets/dc/*.sql`, lands each result under the\nnested layout:\n\n DATA_ROOT/\u003corg_id_15>/\u003cagent_api_name>__\u003cagent_version>/\u003csid>/dc.\u003cname>.json\n\n…via `storage.save`, and emits a `dc._session_manifest.json` summarizing\ncounts, timings, and empty-by-design reasons.\n\nDesign contract:\n - SQL loaded via `dc.load_sql(name, **params)` only — no inline concat.\n - Responses parsed via `dc.parse(response)` — no inline dict digging.\n - Persistence via `storage.save(data, org, agent, ver, sid, \"dc\", name)`\n — no direct writes. Every on-disk path is validated by\n ``paths.session_dir`` before it's touched.\n - Identity is resolved BEFORE the first write. The sessions + participants\n queries run first so (org_id_15, agent_api_name, agent_version) can be\n derived; those three segments name the session dir.\n - Every output file is written exactly once; empty artifacts go through\n `_fetch_empty(name, reason)` so the manifest records why.\n - Rerunning the same session overwrites prior artifacts.\n\nInvocation:\n python3 scripts/fetch_dc.py --session \u003cuuid> [--org \u003calias>] [--verbose]\n\n`--org` defaults to the sf CLI default target org (`sf config get target-org`).\n\nAfter the waterfall finishes, the fetcher chains two downstream steps:\n 1. assemble_dc.main_for_session → dc._session_tree.json\n (skip with --no-assemble).\n 2. render_dc.main_for_session → dc._session_summary.md\n (skip with --no-render). Runs against the on-disk tree from step 1\n or a prior run.\n\n## DC-access preflight\n\nBefore the waterfall starts, we run a single cheap probe:\n\n SELECT Id FROM ssot__AIAgentSession__dlm WHERE ssot__Id__c = '\u003cesc_sid>' LIMIT 1\n\nIf the probe fails (401/403, no org alias, resolve_org failure), we raise\n``DcAccessDenied`` and the top-level ``main()`` branches on\n``sys.stdin.isatty()``:\n\n - Interactive (tty): print a 2-option menu, read one line from stdin.\n (1) retry with a different --org (2) cancel.\n - Headless (no tty): emit a single-line JSON preamble to stdout and\n ``exit(10)`` immediately. Orchestrators (subagents, CI, ``claude -p``)\n see ``exit(10)`` as the stable contract for \"DC access denied\".\n\n``exit(10)`` is the stable contract between this skill and any orchestrator\nwrapper — a non-zero exit code that is distinct from generic errors.\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport html\nimport json\nimport re\nimport subprocess\nimport sys\nimport time\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent))\n\nfrom config import DATA_ROOT, paths, sql as _sql_mod\nfrom dc import SQL_DIR, DCQueryError, default_target_org, load_sql, post, resolve_org\nfrom storage import save\n\n\n# ---- preflight / exit contracts -------------------------------------------\n\n# Exit code reserved for \"DC access denied\".\n# Orchestrators grep for this specifically. See module docstring for the full\n# contract; do not reuse for other failure modes.\nEXIT_DC_ACCESS_DENIED = 10\n\n\nclass DcAccessDenied(RuntimeError):\n \"\"\"DC probe failed in a way that tells us DC is unusable for this session.\n\n Carries both a short machine-readable ``reason`` (for the JSON preamble\n payload — e.g. ``\"401\"``, ``\"no_org\"``, ``\"resolve_org_failed\"``) and a\n longer human-readable ``detail`` (surfaced in the interactive prompt).\n \"\"\"\n\n def __init__(self, reason: str, detail: str) -> None:\n self.reason = reason\n self.detail = detail\n super().__init__(f\"{reason}: {detail}\")\n\n\ndef preflight_dc_access(\n session_id: str,\n org_alias: str,\n) -> tuple[str, str]:\n \"\"\"Probe DC for this session. Returns (instance_url, token) on success.\n\n Runs the cheapest possible DC query — a single ``LIMIT 1`` against\n ``ssot__AIAgentSession__dlm`` — to establish that:\n - ``sf`` CLI can resolve the org alias to an instance URL + token.\n - DC accepts the token (no 401/403).\n - The session id is well-formed SOQL (no literal injection).\n\n Raises ``DcAccessDenied`` on failure. The ``session_id`` is escaped\n via ``sql._escape_sql_literal`` before being interpolated, matching\n the Batch-C defense for ``_session_row_live``. The probe does NOT\n require the session to exist — a zero-row response is success\n (proves DC is reachable and auth'd).\n \"\"\"\n # Stage 1: resolve org alias → instance_url + token. Any failure here\n # is classified as \"no_org\" / \"resolve_org_failed\" — we never got far\n # enough to hit DC.\n try:\n instance_url, token = resolve_org(org_alias)\n except SystemExit as e:\n raise DcAccessDenied(\"no_org\", str(e)) from e\n\n # Stage 2: issue the probe. Reuse ``sessions`` template with a narrow\n # WHERE clause + LIMIT 1. The escape hardens against injection at the\n # literal interpolation point.\n esc_sid = _sql_mod._escape_sql_literal(session_id)\n probe_sql = (\n f\"SELECT ssot__Id__c FROM ssot__AIAgentSession__dlm \"\n f\"WHERE ssot__Id__c = '{esc_sid}' LIMIT 1\"\n )\n try:\n post(probe_sql, instance_url, token, \"preflight\")\n except DCQueryError as e:\n msg = str(e)\n # Classify based on the HTTP code in the error message. The error\n # body carries ``http=\u003ccode>`` right at the start per dc.post().\n if \"http=401\" in msg:\n raise DcAccessDenied(\"401\", msg) from e\n if \"http=403\" in msg:\n raise DcAccessDenied(\"403\", msg) from e\n raise DcAccessDenied(\"dc_probe_failed\", msg) from e\n return instance_url, token\n\n\ndef _emit_dc_access_denied_preamble(\n reason: str,\n detail: str,\n) -> None:\n \"\"\"Headless fallback: emit the machine-readable preamble to stdout.\n\n Single-line JSON per the Batch-D plan. Followed by ``exit(EXIT_DC_ACCESS_DENIED)``\n by the caller. Kept separate so tests can drive it without spawning a\n subprocess.\n \"\"\"\n payload = {\n \"status\": \"DC_ACCESS_DENIED\",\n \"reason\": reason,\n \"detail\": detail,\n \"options\": [\n {\"code\": \"1\", \"action\": \"retry\", \"arg\": \"--org \u003calias>\"},\n {\"code\": \"2\", \"action\": \"cancel\"},\n ],\n }\n print(json.dumps(payload))\n\n\ndef _handle_dc_access_denied(\n exc: DcAccessDenied,\n *,\n session_id: str,\n is_tty: bool,\n) -> int:\n \"\"\"Branch on tty presence and return an exit code.\n\n Interactive: print the 2-option menu; read one line from stdin.\n - ``1`` → signal caller to retry (returns ``EXIT_DC_ACCESS_DENIED``;\n outer ``main()`` may re-prompt for alias in a bounded loop — for\n now we surface the exit code and let the user re-invoke).\n - ``2`` → return 0 (user cancelled; graceful exit).\n\n Headless: emit JSON preamble + ``EXIT_DC_ACCESS_DENIED`` immediately.\n \"\"\"\n if not is_tty:\n _emit_dc_access_denied_preamble(exc.reason, exc.detail)\n return EXIT_DC_ACCESS_DENIED\n\n _log(\n f\"\\nThis skill requires Data Cloud access, which failed: \"\n f\"{exc.detail}\\nYour options:\\n\"\n f\" (1) Retry with a different org alias (pass --org \u003calias>)\\n\"\n f\" (2) Cancel\\nPick one: \"\n )\n try:\n choice = sys.stdin.readline().strip()\n except (KeyboardInterrupt, EOFError):\n return 0\n if choice == \"1\":\n _log(\"Re-invoke with: python3 fetch_dc.py --session \"\n f\"{session_id} --org \u003cnew-alias>\")\n return EXIT_DC_ACCESS_DENIED\n # Any other response is treated as cancel.\n return 0\n\n\n# ---- constants -------------------------------------------------------------\n\n_INTERNAL_TRACE_RE = re.compile(r'\"internalTraceId\":\"([a-f0-9]+)\"') # @rule-suppress starter-sec-002 — re.compile, not exec/eval\n_NOT_SET = {\"\", \"NOT_SET\", None}\n# Mirrors _shared/fs_guard.API_NAME_RE. Duplicated locally as a pre-flight\n# gate so the cross-role fallback's adopted api_name string satisfies the\n# path-builder's regex before we hand it to paths.session_dir(). Without\n# this, paths.session_dir would reject the path with an opaque\n# ValidationError after the fallback claimed success — operator confusion.\n# Anchored \\A...\\Z + `.fullmatch()` (not ^...$ + .match) so trailing\n# newlines don't slip through; matches the security pattern in fs_guard.\n_API_NAME_RE = re.compile(r\"\\A[A-Za-z0-9_]+\\Z\") # @rule-suppress starter-sec-002 — re.compile, not exec/eval\n# Mirrors _shared/fs_guard.AGENT_VERSION_RE. Used by the cross-role\n# fallback so we never adopt a malformed version string into a session-dir\n# path.\n_VERSION_RE = re.compile(r\"\\Av[0-9]+\\Z\") # @rule-suppress starter-sec-002 — re.compile, not exec/eval\n\n# All 24 .sql templates — checked for existence at startup.\n_TEMPLATES = (\n \"sessions\", \"interactions\", \"messages\", \"moments\", \"participants\",\n \"tag_associations\", \"gateway_requests\",\n \"steps\", \"moment_interactions\",\n \"telemetry_spans\", \"generations\", \"app_generation\",\n \"gateway_request_tags\", \"gateway_responses\", \"gateway_records\",\n \"gateway_request_metadata\", \"gateway_request_llm\",\n \"feedback\",\n \"content_quality\", \"content_category\", \"feedback_details\",\n \"tag_definitions\", \"tag_definition_associations\", \"tags\",\n)\n\n\n# ---- small helpers ---------------------------------------------------------\n\ndef _in_list(ids: list[str]) -> str:\n \"\"\"SQL `IN (…)` fragment. Dedupes, drops NOT_SET/empty, preserves order.\"\"\"\n seen: dict[str, None] = {}\n for i in ids:\n if i and i not in _NOT_SET and i not in seen:\n seen[i] = None\n return \"(\" + \",\".join(f\"'{i}'\" for i in seen) + \")\"\n\n\ndef _extract_trace_ids(interactions: list[dict]) -> list[str]:\n \"\"\"Pull runtime trace_ids from interaction rows.\n\n `ssot__TelemetryTraceId__c` is often empty — the real trace_id lives\n HTML-escaped inside `ssot__AttributeText__c` as `\"internalTraceId\":\"…\"`.\n See references/dc_dmo_fields.md for the full gotcha.\n \"\"\"\n out: list[str] = []\n for r in interactions:\n tid = r.get(\"ssot__TelemetryTraceId__c\") or \"\"\n if tid and tid not in _NOT_SET:\n out.append(tid)\n continue\n attr = html.unescape(r.get(\"ssot__AttributeText__c\") or \"\")\n m = _INTERNAL_TRACE_RE.search(attr)\n if m:\n out.append(m.group(1))\n # dedupe, preserve order\n return list(dict.fromkeys(out))\n\n\ndef _preflight_templates() -> None:\n \"\"\"Refuse to start if any .sql template is missing.\"\"\"\n missing = [n for n in _TEMPLATES if not (SQL_DIR / f\"{n}.sql\").is_file()]\n if missing:\n raise SystemExit(\n f\"missing SQL templates in {SQL_DIR}: {', '.join(missing)}\"\n )\n\n\n# ---- fetch helpers (single entry point for disk writes + manifest) --------\n\ndef _log(msg: str) -> None:\n print(msg, file=sys.stderr, flush=True)\n\n\ndef _fetch(\n ctx: dict,\n wave: int,\n name: str,\n where: str,\n order_by: str = \"\",\n *,\n join_path: str | None = None,\n) -> list[dict]:\n \"\"\"Run a live query, save result, append manifest entry, return rows.\"\"\"\n sql = load_sql(name, WHERE_CLAUSE=where, ORDER_BY=order_by)\n if ctx[\"verbose\"]:\n _log(f\" SQL[{name}]:\\n{sql}\\n\")\n t0 = time.monotonic()\n try:\n rows = post(sql, ctx[\"instance_url\"], ctx[\"token\"], name)\n except DCQueryError as e:\n elapsed_ms = int((time.monotonic() - t0) * 1000)\n _log(f\" dc.{name:\u003c30} ERROR ({elapsed_ms}ms)\")\n entry = {\n \"name\": name, \"wave\": wave, \"rows\": 0,\n \"elapsed_ms\": elapsed_ms, \"status\": \"error\",\n \"_unavailable_reason\": str(e).splitlines()[0],\n }\n ctx[\"queries\"].append(entry)\n return []\n elapsed_ms = int((time.monotonic() - t0) * 1000)\n # Only persist when there's data. Zero-row / error / skipped outcomes\n # are recorded in the manifest (status + _unavailable_reason); a\n # matching dc.\u003cname>.json would be an empty `[]` file adding noise to\n # the per-session directory. assemble_dc._load tolerates missing\n # files by returning [].\n if rows:\n save(\n rows,\n ctx[\"org_id_15\"],\n ctx[\"agent_api_name\"],\n ctx[\"agent_version\"],\n ctx[\"session_id\"],\n \"dc\",\n name,\n )\n entry = {\n \"name\": name, \"wave\": wave, \"rows\": len(rows),\n \"elapsed_ms\": elapsed_ms,\n \"status\": \"ok\" if rows else \"empty\",\n }\n if join_path:\n entry[\"join_path\"] = join_path\n if not rows:\n entry[\"_unavailable_reason\"] = (\n f\"query returned zero rows for where={where[:120]}\"\n )\n ctx[\"queries\"].append(entry)\n _log(f\" dc.{name:\u003c30} rows={len(rows):\u003c5} ({elapsed_ms}ms)\")\n return rows\n\n\ndef _fetch_empty(ctx: dict, wave: int, name: str, reason: str) -> None:\n \"\"\"Record a skipped query in the manifest — no HTTP call, no artifact file.\"\"\"\n ctx[\"queries\"].append({\n \"name\": name, \"wave\": wave, \"rows\": 0,\n \"elapsed_ms\": 0, \"status\": \"skipped\",\n \"_unavailable_reason\": reason,\n })\n _log(f\" dc.{name:\u003c30} SKIPPED ({reason})\")\n\n\n# ---- main entry point -----------------------------------------------------\n\ndef main() -> int:\n ap = argparse.ArgumentParser(\n prog=\"fetch_dc.py\",\n description=\"Fetch all 24 DC artifacts for one Agentforce session.\",\n )\n ap.add_argument(\"--session\", required=True,\n help=\"AI-agent session UUID, OR a Salesforce MessagingSession id \"\n \"(0Mw... prefix). Messaging ids are resolved to the UUID \"\n \"via a one-row DC lookup before the waterfall starts.\")\n ap.add_argument(\"--org\", help=\"sf org alias (default: the sf CLI default target org)\")\n ap.add_argument(\"--verbose\", action=\"store_true\", help=\"dump each SQL before POST\")\n ap.add_argument(\"--no-assemble\", action=\"store_true\",\n help=\"skip tree assembly after fetch\")\n ap.add_argument(\"--no-render\", action=\"store_true\",\n help=\"skip summary markdown rendering\")\n # Runtime-agnostic path overrides; default to ~/.vibe/...\n from _shared.cli_override import add_cli_flags, apply_overrides\n add_cli_flags(ap)\n args = ap.parse_args()\n apply_overrides(args, caller_globals=globals())\n\n _preflight_templates()\n\n # No --org means the sf CLI default target org. If none is set, surface\n # it through the DC-access-denied contract (reason \"no_org\", exit 10\n # headless) — the same path an unresolvable alias takes.\n org = args.org\n if not org:\n try:\n org = default_target_org()\n except SystemExit as e:\n return _handle_dc_access_denied(\n DcAccessDenied(\"no_org\", str(e)),\n session_id=args.session,\n is_tty=sys.stdin.isatty(),\n )\n\n # Accept either a UUID or a MessagingSession id (0Mw...). The resolver\n # passes UUIDs through unchanged; on messaging ids it tries disk first\n # (any prior fetch left dc.sessions.json behind under DATA_ROOT), and\n # falls back to a live one-row DC lookup only if disk misses. This keeps\n # fetch_dc consistent with every other entry point and avoids a\n # round-trip when re-invoking against an already-fetched session.\n # On multi-match or zero-match the resolver exits with a diagnostic.\n from resolve_session import is_messaging_id, resolve_disk_or_live\n input_id = args.session\n session_id = resolve_disk_or_live(input_id, org=org)\n if is_messaging_id(input_id):\n _log(f\"resolved messaging id {input_id} → AiAgentSession {session_id}\")\n\n # Preflight — one cheap probe before the waterfall starts.\n # Fails fast with DcAccessDenied on 401/403/no-org. The outer try\n # block routes the exception through _handle_dc_access_denied, which\n # emits either an interactive prompt or a JSON preamble depending on\n # tty presence, then exits with EXIT_DC_ACCESS_DENIED (10).\n try:\n instance_url, token = preflight_dc_access(session_id, org)\n except DcAccessDenied as exc:\n return _handle_dc_access_denied(\n exc,\n session_id=session_id,\n is_tty=sys.stdin.isatty(),\n )\n _log(f\"org: {instance_url}\")\n _log(f\"session: {session_id}\\n\")\n\n ctx = {\n \"session_id\": session_id,\n \"org_alias\": org,\n \"instance_url\": instance_url,\n \"token\": token,\n \"verbose\": args.verbose,\n \"queries\": [],\n \"started_at\": datetime.now(timezone.utc),\n # Populated by _resolve_identity() before the first storage.save call.\n \"org_id_15\": None,\n \"agent_api_name\": None,\n \"agent_version\": None,\n }\n\n _run_waterfall(ctx)\n\n _write_manifest(ctx)\n\n if not args.no_assemble:\n from assemble_dc import main_for_session as _assemble\n _assemble(args.session)\n\n if not args.no_render:\n from render_dc import main_for_session as _render\n _render(args.session) # SystemExits if tree is missing\n\n return 0\n\n\ndef _fetch_no_save(\n ctx: dict, wave: int, name: str, where: str, order_by: str = \"\",\n) -> list[dict]:\n \"\"\"Variant of ``_fetch`` that skips the on-disk write.\n\n Used for the identity-resolution phase: ``sessions`` and ``participants``\n run before we know ``(org_id_15, agent_api_name, agent_version)``, which\n are required to build the target session dir. We stash the rows and\n write them via ``storage.save`` once identity is resolved.\n\n Manifest entry is still appended so the final ``dc._session_manifest.json``\n mirrors every query run.\n \"\"\"\n sql = load_sql(name, WHERE_CLAUSE=where, ORDER_BY=order_by)\n if ctx[\"verbose\"]:\n _log(f\" SQL[{name}]:\\n{sql}\\n\")\n t0 = time.monotonic()\n try:\n rows = post(sql, ctx[\"instance_url\"], ctx[\"token\"], name)\n except DCQueryError as e:\n elapsed_ms = int((time.monotonic() - t0) * 1000)\n _log(f\" dc.{name:\u003c30} ERROR ({elapsed_ms}ms)\")\n ctx[\"queries\"].append({\n \"name\": name, \"wave\": wave, \"rows\": 0,\n \"elapsed_ms\": elapsed_ms, \"status\": \"error\",\n \"_unavailable_reason\": str(e).splitlines()[0],\n })\n return []\n elapsed_ms = int((time.monotonic() - t0) * 1000)\n entry = {\n \"name\": name, \"wave\": wave, \"rows\": len(rows),\n \"elapsed_ms\": elapsed_ms,\n \"status\": \"ok\" if rows else \"empty\",\n }\n if not rows:\n entry[\"_unavailable_reason\"] = (\n f\"query returned zero rows for where={where[:120]}\"\n )\n ctx[\"queries\"].append(entry)\n _log(f\" dc.{name:\u003c30} rows={len(rows):\u003c5} ({elapsed_ms}ms) [deferred save]\")\n return rows\n\n\ndef _lookup_org_id_via_sf_cli(org_alias: str) -> str:\n \"\"\"Return the 15-char org id from ``sf org display --target-org \u003calias>``.\n\n Fallback path for ``_resolve_identity``: the DMO field\n ``ssot__InternalOrganizationId__c`` is occasionally null on\n otherwise-well-formed sessions (materialization gap in the SSOT\n pipeline). Since the authenticated ``sf`` alias already knows the\n org id, we ask the CLI directly rather than failing the whole run.\n\n Matches the shell pattern used by ``dc.resolve_org`` — same argv, same\n JSON shape (``.result.id``). Any non-zero exit, missing binary, or\n malformed payload propagates to the caller as an exception so the\n outer ``_resolve_identity`` can raise a unified \"both sources failed\"\n diagnostic.\n \"\"\"\n proc = subprocess.run(\n [\"sf\", \"org\", \"display\", \"--target-org\", org_alias, \"--json\"],\n capture_output=True, text=True, check=True,\n )\n payload = json.loads(proc.stdout)\n org_id = (payload.get(\"result\") or {}).get(\"id\") or \"\"\n if len(org_id) \u003c 15:\n raise ValueError(\n f\"sf org display returned result.id={org_id!r} (len\u003c15)\"\n )\n return org_id[:15]\n\n\ndef _resolve_identity(\n sessions: list[dict],\n participants: list[dict],\n org_alias: str,\n) -> tuple[str, str, str]:\n \"\"\"Derive (org_id_15, agent_api_name, agent_version) from wave-1 rows.\n\n - ``org_id_15`` — ``sessions[0].ssot__InternalOrganizationId__c[:15]``,\n falling back to ``sf org display --target-org \u003corg_alias>`` when the\n DMO field is null/empty/shorter-than-15 (a known SSOT materialization\n gap; the authenticated CLI alias knows the org id regardless).\n - ``agent_api_name`` / ``agent_version`` — first AGENT participant row\n (sorted by participant id for determinism) with both fields populated.\n\n **Dominant-agent policy:** if multiple AGENT participants are present\n (handoff sessions), the one with the lexicographically first\n ``agent_api_name`` wins. Matches the ``sorted(agents_observed)[0]``\n rule used by ``_session_row_live`` so every writer agrees on the\n session's home dir.\n\n Raises ``SystemExit`` with a clear diagnostic when any segment cannot\n be derived. We deliberately do NOT fall back to a catch-all \"unknown\"\n agent dir — writing under a synthetic identity would let every\n ambiguous session pile into the same folder and corrupt the layout\n invariant downstream readers depend on.\n \"\"\"\n if not sessions:\n raise SystemExit(\n \"fetch_dc: sessions query returned 0 rows; \"\n \"cannot resolve org identity — is the session id correct?\"\n )\n org_id_18 = sessions[0].get(\"ssot__InternalOrganizationId__c\") or \"\"\n if org_id_18 and len(org_id_18) >= 15:\n org_id_15 = org_id_18[:15]\n else:\n # DMO field is null/empty/short. The session is otherwise well-formed\n # — fall back to the authenticated sf alias for the org id.\n try:\n org_id_15 = _lookup_org_id_via_sf_cli(org_alias)\n except (subprocess.CalledProcessError, FileNotFoundError,\n json.JSONDecodeError, ValueError) as e:\n raise SystemExit(\n f\"fetch_dc: both DMO field and sf org display failed — \"\n f\"sessions[0].ssot__InternalOrganizationId__c={org_id_18!r}, \"\n f\"sf org display --target-org {org_alias!r} error: \"\n f\"{type(e).__name__}: {e}\"\n )\n _log(\n f\" org_id_15 fallback to sf org display: {org_id_15} \"\n f\"(DMO field was null)\"\n )\n\n # Collect every AGENT participant with a non-empty agent_api_name +\n # agent_version. Sort by (api_name, version) for a stable dominant-\n # agent pick on handoffs.\n candidates = sorted(\n {\n (\n p.get(\"ssot__AiAgentApiName__c\") or \"\",\n p.get(\"ssot__AiAgentVersionApiName__c\") or \"\",\n )\n for p in participants\n if p.get(\"ssot__AiAgentSessionParticipantRole__c\") == \"AGENT\"\n }\n )\n # Drop entries missing either field.\n candidates = [(n, v) for n, v in candidates if n and v and n not in _NOT_SET and v not in _NOT_SET]\n if not candidates:\n # Fallback: some agent shapes (e.g. MyAgent on Messaging)\n # leave AGENT rows with api_name/version=NOT_SET while USER (or\n # other-role) participant rows carry both fields. Harvest from\n # ANY role first so we land on the real (api_name, version) pair\n # — that's what assemble_dc later reconciles into the tree, so\n # the dir name and tree.identity will agree out of the gate\n # rather than dir=__v0/ + tree=__v24/.\n cross_role = sorted({\n (\n p.get(\"ssot__AiAgentApiName__c\") or \"\",\n p.get(\"ssot__AiAgentVersionApiName__c\") or \"\",\n )\n for p in participants\n })\n cross_role = [\n (n, v) for n, v in cross_role\n if n and n not in _NOT_SET\n and v and v not in _NOT_SET\n and _API_NAME_RE.fullmatch(n)\n and _VERSION_RE.fullmatch(v)\n ]\n if cross_role:\n api_name, version = cross_role[0]\n _log(\n f\" identity fallback: strict AGENT-row resolution failed; \"\n f\"using api_name={api_name!r} version={version!r} from a \"\n f\"non-AGENT participant row\"\n )\n return org_id_15, api_name, version\n # Last-ditch fallback: api_name from any role, but no version\n # available anywhere. Stamp version='v0' (placeholder satisfying\n # ^v[0-9]+$). Without this, the entire DC pipeline is unrunnable\n # for those sessions even though all the downstream rows exist.\n # The Builder Previewer shape legitimately lands here — every\n # version_api_name in DC for that session truly is empty/v0.\n any_names = sorted({\n (p.get(\"ssot__AiAgentApiName__c\") or \"\")\n for p in participants\n })\n any_names = [n for n in any_names if n and n not in _NOT_SET]\n # Only promote names that satisfy fs_guard's API_NAME_RE — otherwise\n # paths.session_dir would reject the dir later with an opaque\n # ValidationError. Cleaner UX is to fall through to the original\n # SystemExit, which carries actionable diagnostic text.\n any_names = [n for n in any_names if _API_NAME_RE.fullmatch(n)]\n if any_names:\n _log(\n f\" identity fallback: strict AGENT-row resolution failed; \"\n f\"no participant row carries (api_name, version) together; \"\n f\"using api_name={any_names[0]!r} from any participant row, \"\n f\"version='v0' (placeholder)\"\n )\n return org_id_15, any_names[0], \"v0\"\n raise SystemExit(\n \"fetch_dc: no AGENT participants with agent_api_name + \"\n \"agent_version on this session; cannot resolve identity. \"\n \"If the session was created recently, STDM materialization \"\n \"may not have completed yet — retry in a few minutes. \"\n \"Otherwise the session may be malformed.\"\n )\n agent_api_name, agent_version = candidates[0]\n return org_id_15, agent_api_name, agent_version\n\n\ndef _run_waterfall(ctx: dict) -> None:\n \"\"\"5-wave forward-only orchestration.\n\n Wave 1 fetches session-scoped rows directly keyed on the session id.\n Wave 2 fetches interaction-scoped rows (Steps, MomentInteractions).\n Wave 3 fans out to Generation (via Step.GenerationId) and to the full\n Gateway audit chain (via GatewayRequest.sessionId__c harvested in\n wave 1 — GatewayResponse, Tags, ObjRecord, Metadata, LLM).\n Wave 4 fetches Generation-scoped child rows (Quality, Category,\n Feedback children).\n Wave 5 fetches the agent/tag catalog (not session-keyed).\n\n Every edge is forward from Session. See `references/dc_dmo_fields.md`\n for the full join map.\n\n Wave-1 ordering: ``sessions`` + ``participants`` run first with\n deferred saves so ``_resolve_identity`` can compute the session's\n target dir BEFORE any on-disk write. Without this ordering, the\n first ``storage.save`` call would fail validation (``paths.session_dir``\n requires the 3 identity segments). Once identity is stamped on\n ``ctx``, remaining wave-1 queries save through the normal ``_fetch``\n path.\n \"\"\"\n sid = ctx[\"session_id\"]\n sid_q = f\"'{sid}'\"\n\n # ---- wave 1a: identity resolution (deferred save) --------------------\n _log(\"== wave 1a: identity resolution ==\")\n sessions = _fetch_no_save(ctx, 1, \"sessions\",\n f\"ssot__Id__c = {sid_q}\")\n participants = _fetch_no_save(ctx, 1, \"participants\",\n f\"ssot__AiAgentSessionId__c = {sid_q}\")\n (\n ctx[\"org_id_15\"],\n ctx[\"agent_api_name\"],\n ctx[\"agent_version\"],\n ) = _resolve_identity(sessions, participants, ctx[\"org_alias\"])\n _log(\n f\" identity: org={ctx['org_id_15']} \"\n f\"agent={ctx['agent_api_name']} version={ctx['agent_version']}\"\n )\n\n # Flush deferred writes now that identity is known. Empty results\n # stay unwritten (same contract as ``_fetch``).\n if sessions:\n save(\n sessions, ctx[\"org_id_15\"], ctx[\"agent_api_name\"],\n ctx[\"agent_version\"], sid, \"dc\", \"sessions\",\n )\n if participants:\n save(\n participants, ctx[\"org_id_15\"], ctx[\"agent_api_name\"],\n ctx[\"agent_version\"], sid, \"dc\", \"participants\",\n )\n\n # ---- wave 1b: remaining session-scoped (5 queries) -------------------\n _log(\"\\n== wave 1b: session-scoped ==\")\n interactions = _fetch(ctx, 1, \"interactions\",\n f\"ssot__AiAgentSessionId__c = {sid_q}\",\n \"ORDER BY ssot__StartTimestamp__c\")\n messages = _fetch(ctx, 1, \"messages\",\n f\"ssot__AiAgentSessionId__c = {sid_q}\",\n \"ORDER BY ssot__MessageSentTimestamp__c\")\n moments = _fetch(ctx, 1, \"moments\",\n f\"ssot__AiAgentSessionId__c = {sid_q}\",\n \"ORDER BY ssot__StartTimestamp__c\")\n _fetch(ctx, 1, \"tag_associations\",\n f\"ssot__AiAgentSessionId__c = {sid_q}\")\n # sessionId__c is stored as a literal quoted string ('\"\u003cuuid>\"'); raw-UUID\n # exact match returns 0 rows. LIKE handles both '\"\u003cuuid>\"' and any future\n # variant cleanly. See references/dc_dmo_fields.md.\n gw_requests = _fetch(ctx, 1, \"gateway_requests\",\n f\"sessionId__c LIKE '%{sid}%'\",\n \"ORDER BY timestamp__c\")\n\n # Harvest IDs from wave 1 for downstream waves\n interaction_ids = [r.get(\"ssot__Id__c\") for r in interactions]\n # Agent name harvest: moments DMO is sparse — many short or\n # abandoned-before-tagging sessions never write a moment row, leaving\n # agent_api_names empty even though the agent identity was already\n # resolved upstream. Fall through to participants (always populated\n # for any session with ≥1 turn) and finally to ctx.agent_api_name\n # (resolved during identity wave 1a from BotDefinition).\n _harvested = {\n r[\"ssot__AiAgentApiName__c\"]\n for r in moments\n if r.get(\"ssot__AiAgentApiName__c\") and r[\"ssot__AiAgentApiName__c\"] not in _NOT_SET\n }\n _harvested |= {\n r[\"ssot__AiAgentApiName__c\"]\n for r in participants\n if r.get(\"ssot__AiAgentApiName__c\") and r[\"ssot__AiAgentApiName__c\"] not in _NOT_SET\n }\n if not _harvested and ctx.get(\"agent_api_name\"):\n _harvested = {ctx[\"agent_api_name\"]}\n agent_api_names = sorted(_harvested)\n gw_req_ids = [r.get(\"gatewayRequestId__c\") for r in gw_requests]\n trace_ids = _extract_trace_ids(interactions)\n _log(f\" harvested: {len(interaction_ids)} interactions, \"\n f\"{len(moments)} moments, trace_ids={len(trace_ids)}, \"\n f\"gw_req_ids={len(gw_req_ids)}, agents={agent_api_names}\")\n\n # ---- wave 2: interaction/moment-scoped (2 queries) --------------------\n _log(\"\\n== wave 2: interaction/moment-scoped ==\")\n interaction_in = _in_list(interaction_ids)\n if interaction_in != \"()\":\n steps = _fetch(ctx, 2, \"steps\",\n f\"ssot__AiAgentInteractionId__c IN {interaction_in}\",\n \"ORDER BY ssot__StartTimestamp__c\")\n _fetch(ctx, 2, \"moment_interactions\",\n f\"ssot__AiAgentInteractionId__c IN {interaction_in}\")\n else:\n steps = []\n _fetch_empty(ctx, 2, \"steps\",\n \"no interactions for session\")\n _fetch_empty(ctx, 2, \"moment_interactions\",\n \"no interactions for session\")\n\n # Harvest generation IDs from steps for forward fetch of GenAIGeneration.\n # (Step.ssot__GenAiGatewayResponseId__c / ssot__GenAiGatewayRequestId__c are\n # NOT harvested — those would be backward chains. Gateway audit rows are\n # fetched forward from the session via GatewayRequest.sessionId__c in wave 1.)\n step_gen_ids = [s.get(\"ssot__GenerationId__c\") for s in steps]\n _log(f\" harvested: generation_ids={len([x for x in step_gen_ids if x and x not in _NOT_SET])}\")\n\n # ---- wave 3: trace/generation/gateway fanout (6 queries) --------------\n _log(\"\\n== wave 3: trace/generation/gateway fanout ==\")\n\n # telemetry_spans — on trace_ids from interactions\n trace_in = _in_list(trace_ids)\n if trace_in != \"()\":\n _fetch(ctx, 3, \"telemetry_spans\",\n f\"ssot__TelemetryTrace__c IN {trace_in}\",\n \"ORDER BY ssot__StartDateTime__c\")\n else:\n _fetch_empty(ctx, 3, \"telemetry_spans\",\n \"no trace_ids extractable from interactions \"\n \"(TelemetryTraceId__c empty and no internalTraceId in AttributeText__c)\")\n\n # generations — canonical path: steps.ssot__GenerationId__c\n gen_in = _in_list(step_gen_ids)\n if gen_in != \"()\":\n generations = _fetch(ctx, 3, \"generations\",\n f\"generationId__c IN {gen_in}\",\n \"ORDER BY timestamp__c\",\n join_path=\"steps.ssot__GenerationId__c\")\n # app_generation — same join key as generations (sibling DMO)\n _fetch(ctx, 3, \"app_generation\",\n f\"generationId__c IN {gen_in}\",\n \"ORDER BY timestamp__c\",\n join_path=\"steps.ssot__GenerationId__c\")\n else:\n generations = []\n _fetch_empty(ctx, 3, \"generations\",\n \"no step.ssot__GenerationId__c values on this session \"\n \"(all steps had NOT_SET or steps table was empty)\")\n _fetch_empty(ctx, 3, \"app_generation\",\n \"no step.ssot__GenerationId__c values on this session \"\n \"(all steps had NOT_SET or steps table was empty)\")\n\n # --- Gateway audit chain (forward-only from session) ------------------\n # All audit rows flow forward from Session via GatewayRequest.sessionId__c\n # (harvested in wave 1). Every child table is keyed on the authoritative\n # gw_req_ids from that wave-1 fetch:\n #\n # GenAIGatewayResponse generationRequestId__c IN {gw_req_ids}\n # GenAIGatewayRequestTag parent__c IN {gw_req_ids}\n # GenAIGtwyObjRecord parent__c IN {gw_req_ids}\n # GenAIGtwyRequestMetadata parent__c IN {gw_req_ids}\n # GenAIGtwyRequestLLM parent__c IN {gw_req_ids}\n #\n # No Step->Response->Request chain. Step's GenAiGatewayRequestId__c and\n # GenAiGatewayResponseId__c FKs exist but we don't harvest them: they\n # only cover LLM_STEP-owned calls and miss nested features like\n # PromptTemplateGenerationsInvocable. The session-direct fetch in wave 1\n # is the authoritative set.\n gw_req_in = _in_list(gw_req_ids)\n if gw_req_in != \"()\":\n _fetch(ctx, 3, \"gateway_responses\",\n f\"generationRequestId__c IN {gw_req_in}\",\n \"ORDER BY timestamp__c\",\n join_path=\"gateway_requests.gatewayRequestId__c\")\n _fetch(ctx, 3, \"gateway_request_tags\",\n f\"parent__c IN {gw_req_in}\")\n _fetch(ctx, 3, \"gateway_records\",\n f\"parent__c IN {gw_req_in}\")\n _fetch(ctx, 3, \"gateway_request_metadata\",\n f\"parent__c IN {gw_req_in}\")\n _fetch(ctx, 3, \"gateway_request_llm\",\n f\"parent__c IN {gw_req_in}\")\n else:\n gw_empty_reason = (\n \"no gateway_requests matched GatewayRequest.sessionId__c LIKE \"\n \"'%\u003csid>%' in wave 1 — Trust Layer gateway logging may be \"\n \"disabled for this org, or the session produced no LLM calls\"\n )\n for n in (\"gateway_responses\", \"gateway_request_tags\", \"gateway_records\",\n \"gateway_request_metadata\", \"gateway_request_llm\"):\n _fetch_empty(ctx, 3, n, gw_empty_reason)\n\n # feedback — keyed by generationId (no session col on GenAIFeedback)\n gen_ids_for_feedback = [r.get(\"generationId__c\") for r in generations]\n feedback_in = _in_list(gen_ids_for_feedback)\n if feedback_in != \"()\":\n feedback = _fetch(ctx, 3, \"feedback\",\n f\"generationId__c IN {feedback_in}\")\n else:\n feedback = []\n _fetch_empty(ctx, 3, \"feedback\",\n \"no generation_ids available (generations wave was empty)\")\n\n # ---- wave 4: generation/feedback-dependent (3 queries) ----------------\n _log(\"\\n== wave 4: generation/feedback-dependent ==\")\n gen_only_in = _in_list(gen_ids_for_feedback)\n if gen_only_in != \"()\":\n quality = _fetch(ctx, 4, \"content_quality\",\n f\"parent__c IN {gen_only_in}\")\n else:\n quality = []\n _fetch_empty(ctx, 4, \"content_quality\",\n \"no generation_ids (parent__c IN cannot be empty)\")\n\n # content_category — parent is (generation_ids ∪ quality.id__c)\n quality_ids = [r.get(\"id__c\") or r.get(\"ssot__Id__c\") for r in quality]\n category_parent_ids = gen_ids_for_feedback + quality_ids\n category_in = _in_list(category_parent_ids)\n if category_in != \"()\":\n _fetch(ctx, 4, \"content_category\",\n f\"parent__c IN {category_in}\")\n else:\n _fetch_empty(ctx, 4, \"content_category\",\n \"no generation_ids or quality_ids to parent to\")\n\n # feedback_details — parent is feedback row id.\n # GenAIFeedback__dlm PK is `feedbackId__c` (no ssot__ prefix, verified via describe);\n # the old harvest used ssot__Id__c which doesn't exist on this DMO.\n feedback_ids = [r.get(\"feedbackId__c\") for r in feedback]\n feedback_ids_in = _in_list(feedback_ids)\n if feedback_ids_in != \"()\":\n _fetch(ctx, 4, \"feedback_details\",\n f\"parent__c IN {feedback_ids_in}\")\n else:\n _fetch_empty(ctx, 4, \"feedback_details\",\n \"no feedback rows for this session \"\n \"(no user thumbs-up/down on any generation)\")\n\n # ---- wave 5: tag catalog (agent-scoped) -------------------------------\n _log(\"\\n== wave 5: tag catalog ==\")\n # Live orgs use 'Available' (not Help-doc's 'Active'). If that returns\n # zero rows, retry unfiltered — some orgs have a different enum value or\n # the definitions predate the Status field. See references/dc_dmo_fields.md.\n tag_defs = _fetch(ctx, 5, \"tag_definitions\",\n \"ssot__Status__c = 'Available'\")\n if not tag_defs:\n _log(\" (tag_definitions Status='Available' empty — retrying unfiltered)\")\n # Pop the empty entry so _fetch can append a fresh one for the retry.\n # The retry overwrites the same on-disk artifact (last run wins).\n ctx[\"queries\"].pop()\n tag_defs = _fetch(ctx, 5, \"tag_definitions\",\n \"ssot__Id__c IS NOT NULL\")\n # Tag the retry in the manifest for traceability.\n ctx[\"queries\"][-1][\"notes\"] = (\n \"retried unfiltered after Status='Available' returned 0 rows\"\n )\n if not tag_defs:\n ctx[\"queries\"][-1][\"_unavailable_reason\"] = (\n \"tag_definitions truly empty on this org\"\n )\n\n agent_in = _in_list(agent_api_names)\n if agent_in != \"()\":\n _fetch(ctx, 5, \"tag_definition_associations\",\n f\"ssot__AiAgentApiName__c IN {agent_in}\")\n else:\n _fetch_empty(ctx, 5, \"tag_definition_associations\",\n \"no agent api names observed in moments\")\n\n def_ids = [r.get(\"ssot__Id__c\") for r in tag_defs]\n def_in = _in_list(def_ids)\n if def_in != \"()\":\n _fetch(ctx, 5, \"tags\",\n f\"ssot__AiAgentTagDefinitionId__c IN {def_in}\")\n else:\n _fetch_empty(ctx, 5, \"tags\",\n \"no tag_definition ids (tag_definitions was empty)\")\n\n # Tally steps by type for the session_shape classifier.\n steps_by_type = {\n \"LLM_STEP\": 0, \"ACTION_STEP\": 0, \"TOPIC_STEP\": 0,\n \"TRUST_GUARDRAILS_STEP\": 0, \"SESSION_END\": 0,\n }\n for s in steps:\n t = s.get(\"ssot__AiAgentInteractionStepType__c\")\n if t in steps_by_type:\n steps_by_type[t] += 1\n\n # Stash harvested-id counts for the manifest\n steps_with_gen = sum(1 for g in step_gen_ids if g and g not in _NOT_SET)\n ctx[\"harvested_ids\"] = {\n \"sessions\": len(sessions),\n \"messages\": len(messages),\n \"interactions\": len(interaction_ids),\n \"moments\": len(moments),\n \"steps_total\": len(steps),\n \"steps_by_type\": steps_by_type,\n \"steps_with_generation_id\": steps_with_gen,\n \"trace_ids_from_interactions\": len(trace_ids),\n \"gateway_request_ids\": len(gw_req_ids),\n \"generation_ids\": sum(\n 1 for g in gen_ids_for_feedback if g and g not in _NOT_SET\n ),\n \"agents_observed\": agent_api_names,\n }\n\n # Classify session shape. 5-value enum; rules evaluated top-to-bottom, first-match-wins.\n # See references/dc_dmo_fields.md for rationale.\n ctx[\"session_shape\"] = _classify_session_shape(\n sessions_count=len(sessions),\n steps_total=len(steps),\n llm_step_count=steps_by_type[\"LLM_STEP\"],\n steps_with_generation_id=steps_with_gen,\n gw_req_count=len(gw_req_ids),\n )\n\n\ndef _classify_session_shape(*, sessions_count, steps_total, llm_step_count,\n steps_with_generation_id, gw_req_count):\n \"\"\"6-value session-shape diagnostic. First match wins.\n\n Rules (top-to-bottom):\n - session_not_found — sessions.json returned 0 rows\n - interactions_not_materialized_yet — gw_reqs > 0 AND steps == 0. Gateway DMOs\n materialize within minutes; STDM Interaction\n / Step / Message DMOs can take hours to days.\n Detected BEFORE abandoned_before_llm because\n gw_req_count > 0 is a stronger positive signal\n than steps_total > 0 (and the two rules'\n inputs are disjoint on this path — steps == 0\n here, gw_reqs == 0 for abandoned).\n - abandoned_before_llm — steps > 0, LLM_STEP == 0, gw_reqs == 0\n - gateway_requests_dropped_by_stdm — LLM_STEP > 0\n AND gw_reqs == 0 AND steps_with_generation_id == 0.\n STDM dropped not just gateway_requests but also\n generations (the join chain Step→Generation\n is broken). Frequently observed on Atlas-routed\n sessions; visible in Splunk LLMGatewayUsageEventWriter\n even when DC has zero rows.\n - planner_ran_no_gateway_logs — LLM_STEP > 0 AND steps_with_generation_id > 0\n AND gw_reqs == 0 (the extra guard prevents a\n broken generations-IN-clause from being\n misclassified here). Generations wrote to DC,\n gateway_requests did not — narrower defect than\n gateway_requests_dropped_by_stdm.\n - complete — everything else (the \"normal\" bucket,\n including partial chain-orphan sessions)\n\n See references/dc_dmo_fields.md for rationale.\n \"\"\"\n if sessions_count == 0:\n return \"session_not_found\"\n if gw_req_count > 0 and steps_total == 0:\n return \"interactions_not_materialized_yet\"\n if steps_total > 0 and llm_step_count == 0 and gw_req_count == 0:\n return \"abandoned_before_llm\"\n # When LLM_STEPs ran but neither gateway_requests nor generations\n # wrote, the STDM exporter dropped both. Distinct from\n # planner_ran_no_gateway_logs (which still has generation rows).\n if (llm_step_count > 0 and steps_with_generation_id == 0\n and gw_req_count == 0):\n return \"gateway_requests_dropped_by_stdm\"\n if (llm_step_count > 0 and steps_with_generation_id > 0\n and gw_req_count == 0):\n return \"planner_ran_no_gateway_logs\"\n return \"complete\"\n\n\ndef _write_manifest(ctx: dict) -> None:\n \"\"\"Emit dc._session_manifest.json next to the 24 artifacts.\"\"\"\n finished = datetime.now(timezone.utc)\n elapsed_ms = int((finished - ctx[\"started_at\"]).total_seconds() * 1000)\n manifest = {\n \"_doc\": \"Per-query summary of this DC fetch run.\",\n \"session_id\": ctx[\"session_id\"],\n \"org_alias\": ctx[\"org_alias\"],\n \"instance_url\": ctx[\"instance_url\"],\n # Identity resolved in wave 1a, before any on-disk write. Stamped\n # here so downstream readers can recover (org, agent, version)\n # without re-parsing the tree.\n \"org_id_15\": ctx.get(\"org_id_15\"),\n \"agent_api_name\": ctx.get(\"agent_api_name\"),\n \"agent_version\": ctx.get(\"agent_version\"),\n \"session_shape\": ctx.get(\"session_shape\", \"unknown\"),\n \"started_at_utc\": ctx[\"started_at\"].isoformat().replace(\"+00:00\", \"Z\"),\n \"finished_at_utc\": finished.isoformat().replace(\"+00:00\", \"Z\"),\n \"elapsed_ms\": elapsed_ms,\n \"queries\": ctx[\"queries\"],\n \"harvested_ids\": ctx.get(\"harvested_ids\", {}),\n }\n target = paths.session_dir(\n ctx[\"org_id_15\"],\n ctx[\"agent_api_name\"],\n ctx[\"agent_version\"],\n ctx[\"session_id\"],\n ) / \"dc._session_manifest.json\"\n target.parent.mkdir(parents=True, exist_ok=True)\n target.write_text(json.dumps(manifest, indent=2, default=str) + \"\\n\")\n _log(f\"\\nmanifest: {target} ({elapsed_ms}ms total)\")\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":46143,"content_sha256":"c35c8071b5bf920f369460814977b0f4f3e90423ba9d688c8b00ba431cf967c5"},{"filename":"scripts/render_dc.py","content":"\"\"\"Render dc._session_summary.md from dc._session_tree.json + dc._session_manifest.json.\n\nGiven `DATA_ROOT/\u003csid>/dc._session_tree.json` (produced by `scripts/assemble_dc.py`)\nthis module emits a human-readable `dc._session_summary.md`. Pure tree reader —\nno raw DMO fetches, no joins. The `session.identity` sub-object on the tree\n(added by the assembler) supplies all identity fields; everything else\n(counts, trees, catalog) comes from the tree itself.\n\nOutput has 8 `##` sections:\n\n 1. Session identity — org/agent/bot/planner/session-start/end/duration\n 2. ID reference — full UUIDs for every ellipsized id in the tree\n 3. Transcript — narrative USER/AGENT text per TURN\n 4. Complete hierarchical trace — tree with `+start + dur = +end` math\n 5. Per-turn summary — one row per interaction\n 6. Session counts — engineer-facing totals\n 7. Empties diagnostics — DMOs that returned 0 rows + reason\n 8. Catalog (session-filtered)\n\nContract:\n - Pure in-memory compute over already-produced artifacts.\n - Reads DATA_ROOT/\u003csid>/dc._session_tree.json and dc._session_manifest.json.\n - UUIDs in the tree are truncated to first 8 chars + `…`; full forms\n live in the ID reference block.\n - Session-not-found trees render only Session identity + a note.\n - Tree schema version check: refuses incompatible versions, warns on\n missing version (backward compat).\n\nInvocation:\n python3 scripts/render_dc.py --session \u003csid>\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport html\nimport json\nimport sys\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\n\nsys.path.insert(0, str(Path(__file__).parent))\n\nfrom config import DATA_ROOT, paths\n\n\n# ---- schema ---------------------------------------------------------------\n\n_SUPPORTED_SCHEMA_VERSION = 1\n\n\n# ---- timestamp / string helpers -------------------------------------------\n\ndef _parse_iso(s: Optional[str]) -> Optional[datetime]:\n if not s:\n return None\n try:\n return datetime.fromisoformat(s.replace(\"Z\", \"+00:00\"))\n except (ValueError, AttributeError):\n return None\n\n\ndef _fmt_offset(ts_iso: Optional[str], start_dt: Optional[datetime]) -> str:\n \"\"\"Return '+12.345s' or '—' if timestamp is missing.\"\"\"\n ts = _parse_iso(ts_iso)\n if ts is None or start_dt is None:\n return \"—\"\n return f\"+{(ts - start_dt).total_seconds():.3f}s\"\n\n\ndef _fmt_duration_ms(start_iso: Optional[str], end_iso: Optional[str]) -> str:\n s = _parse_iso(start_iso)\n e = _parse_iso(end_iso)\n if s is None or e is None:\n return \"—\"\n return f\"{int((e - s).total_seconds() * 1000)}ms\"\n\n\ndef _decode(s: Optional[str]) -> str:\n if not s:\n return \"\"\n return html.unescape(s).replace(\"\\n\", \" \").strip()\n\n\ndef _truncate(s: Optional[str], n: int = 80) -> str:\n if not s:\n return \"—\"\n s = _decode(s)\n return s if len(s) \u003c= n else s[: n - 1] + \"…\"\n\n\ndef _short(uid: Optional[str], keep: int = 8) -> str:\n \"\"\"Truncate a UUID to `keep` chars + ellipsis. Full form lives in ID reference.\"\"\"\n if not uid:\n return \"—\"\n return uid[:keep] + \"…\" if len(uid) > keep else uid\n\n\ndef _fmt_total_duration(start_iso: Optional[str], end_iso: Optional[str]) -> Optional[str]:\n s = _parse_iso(start_iso)\n e = _parse_iso(end_iso)\n if s is None or e is None:\n return None\n total_secs = (e - s).total_seconds()\n if total_secs >= 3600:\n h = int(total_secs // 3600)\n m = int((total_secs % 3600) // 60)\n return f\"{h}h {m}m {total_secs % 60:.3f}s\"\n if total_secs >= 60:\n m = int(total_secs // 60)\n return f\"{m}m {total_secs % 60:.3f}s\"\n return f\"{total_secs:.3f}s\"\n\n\n# ---- session-end derivation ----------------------------------------------\n\ndef _derive_session_end(sess: dict) -> tuple[Optional[str], Optional[str]]:\n \"\"\"Return (effective_end_iso, source_label).\n\n Contract §3.5 derivation:\n 1. If `session.end_ts` is non-null → return it as-is (caller adds\n \"✓ materialized\" suffix).\n 2. Else prefer a SESSION_END interaction's `start_ts`\n (label: \"from SESSION_END interaction\").\n 3. Else fall back to the last TURN interaction's `end_ts`\n (or `start_ts` if end is null) (label: \"session still open (last TURN)\").\n 4. Else `(None, None)`.\n \"\"\"\n end_iso = sess.get(\"end_ts\")\n if end_iso:\n return end_iso, None # no derivation needed\n interactions = sess.get(\"interactions\") or []\n # Prefer SESSION_END start_ts.\n for iv in interactions:\n if iv.get(\"type\") == \"SESSION_END\" and iv.get(\"start_ts\"):\n return iv[\"start_ts\"], \"from SESSION_END interaction\"\n # Fall back to the last TURN (by interaction order; tree is already sorted).\n last_turn = None\n for iv in interactions:\n if iv.get(\"type\") == \"TURN\":\n last_turn = iv\n if last_turn is not None:\n fallback = last_turn.get(\"end_ts\") or last_turn.get(\"start_ts\")\n if fallback:\n return fallback, \"session still open (last TURN)\"\n return None, None\n\n\n# ---- section builders -----------------------------------------------------\n\n# Channel-mode value → human cell. Annotated so the reader can tell at\n# a glance why we believe the mode is\n# what it is — `ssot__AiAgentChannelType__c` is identical for MIAW\n# production and Builder Previewer, which makes it useless without the\n# annotation.\n_MODE_HINTS: dict[str, str] = {\n \"production_messaging\": \"production_messaging ← inferred from RelatedMessagingSessionId\",\n \"builder_previewer\": \"builder_previewer ← inferred from VariableText__c bootstrap keys\",\n \"voice\": \"voice ← inferred from RelatedVoiceCallId\",\n \"unknown\": \"unknown ← no signals (headless API, etc.)\",\n}\n\n\ndef _fmt_mode_cell(mode_value: str) -> str:\n \"\"\"Format the `mode` field with a short why-this-value annotation.\"\"\"\n return _MODE_HINTS.get(mode_value, mode_value)\n\n\n# Channel values that flag a production messaging session. `VariableText__c`\n# is expected to be NOT_SET on these — the bootstrap variables ride the\n# messaging session record, not the AI session. Any other channel\n# (E & O headless API, Builder Previewer, voice, unknown/integration)\n# can also legitimately produce NOT_SET, but for a different reason\n# (no bootstrap variables were attached at session start) — and we\n# must not mislabel those as \"production messaging path\".\n_MESSAGING_CHANNELS = frozenset({\n \"SCRT2 - EmbeddedMessaging\",\n \"Messaging\",\n})\n\n\ndef _section_session_bootstrap(identity: dict, channel: Optional[str] = None) -> List[str]:\n \"\"\"Render the `bootstrap_variables` block parsed from VariableText__c.\n\n Three states (all rendered as a small subtable so they don't clutter\n the main identity table):\n - None / NOT_SET → \"no bootstrap variables for this session\"\n (with a messaging-path addendum only when the\n session's `channel` is actually a messaging\n channel — see `_MESSAGING_CHANNELS`).\n - parse error → \"_parse_error\" with the raw prefix\n - populated → key/value pairs sorted, plus a \"Builder Previewer\n indicators\" tally derived from the same key set\n used in `_derive_mode`.\n\n Empty section returned when bootstrap_variables is missing entirely\n (older artifacts predate the bootstrap-variables harvest).\n \"\"\"\n if \"bootstrap_variables\" not in identity:\n return [] # older artifact: no bootstrap_variables harvested\n\n bootstrap = identity.get(\"bootstrap_variables\")\n lines: List[str] = [\"## Session bootstrap\", \"\"]\n\n if bootstrap is None:\n if channel in _MESSAGING_CHANNELS:\n lines.append(\n \"`ssot__VariableText__c` is `NOT_SET` — no bootstrap variables \"\n \"(production messaging path; messaging sessions don't carry VariableText).\"\n )\n else:\n lines.append(\n \"`ssot__VariableText__c` is `NOT_SET` — no bootstrap variables for this session.\"\n )\n lines.append(\"\")\n return lines\n\n if isinstance(bootstrap, dict) and bootstrap.get(\"_parse_error\"):\n raw = bootstrap.get(\"_raw\") or \"\"\n lines.append(\"`ssot__VariableText__c` failed to parse as JSON:\")\n lines.append(\"\")\n lines.append(\"```\")\n lines.append(raw)\n lines.append(\"```\")\n lines.append(\"\")\n return lines\n\n # Populated case.\n indicator_keys = {\n \"__resolved_locale__\",\n \"__locale_instruction__\",\n \"__supports_result_display__\",\n \"__show_tool_results_invoked__\",\n }\n present_indicators = sorted(set(bootstrap) & indicator_keys)\n indicator_cell = (\n \"yes · \" + \", \".join(present_indicators)\n if present_indicators else \"no\"\n )\n\n lines.append(\"| Key | Value |\")\n lines.append(\"|---|---|\")\n for key in sorted(bootstrap):\n value = bootstrap[key]\n # Render lists / dicts compactly; keep strings/numbers/bools as-is.\n if isinstance(value, (list, dict)):\n value_repr = json.dumps(value)\n else:\n value_repr = str(value)\n # Pipe character would break the markdown table — escape.\n value_repr = value_repr.replace(\"|\", \"\\\\|\")\n lines.append(f\"| {key} | {value_repr} |\")\n lines.append(f\"| **Builder Previewer indicators** | {indicator_cell} |\")\n lines.append(\"\")\n return lines\n\n\ndef _compose_agent_cell(identity: dict) -> Optional[str]:\n \"\"\"Build the human Agent cell from identity fields; drop None components.\"\"\"\n parts: List[str] = []\n api_name = identity.get(\"agent_api_name\")\n version = identity.get(\"agent_version\")\n if api_name and version:\n parts.append(f\"{api_name} {version}\")\n elif api_name:\n parts.append(api_name)\n elif version:\n parts.append(version)\n label = identity.get(\"agent_label\")\n if label:\n parts.append(f\"— {label}\" if parts else label)\n atype = identity.get(\"agent_type\")\n if atype:\n parts.append(f\"({atype})\")\n return \" \".join(parts) if parts else None\n\n\ndef _fmt_session_end_cell(effective_end_iso: Optional[str],\n end_source: Optional[str],\n raw_end_iso: Optional[str]) -> Optional[str]:\n \"\"\"Compose the identity-table value for `Session end`.\n\n - Materialized end (`raw_end_iso` truthy) → \"\u003ciso> ✓ materialized\"\n - Derived end (end_source truthy) → \"\u003ciso> (\u003csource label>)\"\n - Neither → None (row is dropped)\n \"\"\"\n if effective_end_iso is None:\n return None\n if raw_end_iso:\n return f\"{effective_end_iso} ✓ materialized\"\n if end_source:\n return f\"{effective_end_iso} ({end_source})\"\n return effective_end_iso\n\n\ndef _section_session_identity(sess: dict, effective_end_iso: Optional[str],\n end_source: Optional[str]) -> List[str]:\n identity = sess.get(\"identity\") or {}\n org = sess.get(\"org\") or {}\n\n # `mode` is \"production_messaging\" / \"builder_previewer\" / \"voice\" /\n # \"unknown\" — derived in assemble_dc from RelatedMessagingSessionId +\n # RelatedVoiceCallId + bootstrap_variables because\n # ssot__AiAgentChannelType__c is identical for MIAW production and\n # Builder Previewer.\n mode_value = identity.get(\"mode\")\n mode_cell = _fmt_mode_cell(mode_value) if mode_value else None\n\n rows_out = [\n (\"Session id\", sess.get(\"id\")),\n (\"Org id\", identity.get(\"org_id\")),\n (\"Org alias\", org.get(\"alias\")),\n (\"Instance URL\", org.get(\"instance_url\")),\n (\"Channel\", sess.get(\"channel\")),\n (\"Mode\", mode_cell),\n (\"App type\", identity.get(\"app_type\")),\n (\"Agent\", _compose_agent_cell(identity)),\n (\"Bot id\", identity.get(\"bot_id\")),\n (\"Bot name\", identity.get(\"bot_name\")),\n (\"Bot version id\", identity.get(\"bot_version_id\")),\n (\"Planner id\", identity.get(\"planner_id\")),\n (\"Planner name\", identity.get(\"planner_name\")),\n (\"Planner type\", identity.get(\"planner_type\")),\n (\"Configured model\", identity.get(\"configured_model\")),\n (\"Platform user id\", identity.get(\"platform_user_id\")),\n (\"Messaging session id\", identity.get(\"messaging_session_id\")),\n (\"Messaging end-user id\", identity.get(\"messaging_end_user_id\")),\n (\"Voice call id\", identity.get(\"voice_call_id\")),\n (\"Individual id\", identity.get(\"individual_id\")),\n (\"Session start\", sess.get(\"start_ts\")),\n (\n \"Session end\",\n _fmt_session_end_cell(effective_end_iso, end_source, sess.get(\"end_ts\")),\n ),\n (\"End type\", sess.get(\"end_type\")),\n (\"Total duration\", _fmt_total_duration(sess.get(\"start_ts\"), effective_end_iso)),\n ]\n lines: List[str] = [\"## Session identity\", \"\", \"| Field | Value |\", \"|---|---|\"]\n for label, value in rows_out:\n if value is None or value == \"\" or value == \"—\":\n continue\n lines.append(f\"| {label} | {value} |\")\n lines.append(\"\")\n return lines\n\n\ndef _section_id_reference(sess: dict) -> List[str]:\n \"\"\"Full-UUID lookup for every id the tree truncates.\"\"\"\n lines: List[str] = [\n \"## ID reference\",\n \"\",\n \"The tree truncates UUIDs to the first 8 chars + `…`. Look up the full \"\n \"form here. Ordered by type → first occurrence.\",\n \"\",\n \"```\",\n f\"session = {sess.get('id')}\",\n \"\",\n ]\n # Interactions\n lines.append(\"interactions:\")\n for iv in sess.get(\"interactions\", []):\n lines.append(\n f\" {iv.get('id')} type={iv.get('type') or '?'} \"\n f\"trace={iv.get('trace_id') or '—'}\"\n )\n lines.append(\"\")\n # Participants\n lines.append(\"participants:\")\n for p in sess.get(\"participants\", []):\n lines.append(\n f\" {p.get('participant_id') or '—'} role={p.get('role') or '—'} \"\n f\"agent={p.get('agent_api_name') or '—'} version={p.get('agent_version') or '—'} \"\n f\"type={p.get('agent_type') or '—'}\"\n )\n lines.append(\"\")\n\n # Steps, generations, gateway_requests, messages harvested from the tree.\n all_steps: List[tuple] = []\n all_gens: List[tuple] = []\n all_gws: List[tuple] = []\n all_msgs: List[tuple] = []\n for iv in sess.get(\"interactions\", []):\n for m in iv.get(\"messages\", []):\n if m.get(\"message_id\"):\n all_msgs.append((m[\"message_id\"], m.get(\"role\") or m.get(\"type\") or \"?\"))\n for st in iv.get(\"steps\", []):\n if st.get(\"id\"):\n all_steps.append((st[\"id\"], st.get(\"type\") or \"?\", st.get(\"name\") or \"—\"))\n gen = st.get(\"generation\")\n if gen and gen.get(\"generation_id\"):\n all_gens.append((\n gen[\"generation_id\"],\n gen.get(\"response_id\") or \"—\",\n gen.get(\"feature\") or \"—\",\n ))\n gw = st.get(\"gateway_request\")\n if gw and gw.get(\"gateway_request_id\"):\n all_gws.append((\n gw[\"gateway_request_id\"], \"declared\",\n gw.get(\"feature\") or \"—\", gw.get(\"model\") or \"—\",\n ))\n for tb in iv.get(\"timestamp_bound_gateway_calls\", []):\n if tb.get(\"gateway_request_id\"):\n all_gws.append((\n tb[\"gateway_request_id\"], \"timestamp_window\",\n tb.get(\"feature\") or \"—\", tb.get(\"model\") or \"—\",\n ))\n for g in sess.get(\"unbound_gateway_calls\", []):\n if g.get(\"gateway_request_id\"):\n all_gws.append((\n g[\"gateway_request_id\"], \"unbound\",\n g.get(\"feature\") or \"—\", g.get(\"model\") or \"—\",\n ))\n\n if all_steps:\n lines.append(\"steps:\")\n for sid_, stype, sname in all_steps:\n lines.append(f\" {sid_} type={stype} name={sname}\")\n lines.append(\"\")\n if all_gens:\n lines.append(\"generations:\")\n for gid, rid, feat in all_gens:\n lines.append(f\" {gid} response_id={rid} feature={feat}\")\n lines.append(\"\")\n if all_gws:\n lines.append(\"gateway_requests:\")\n for gwid, bm, feat, model in all_gws:\n lines.append(f\" {gwid} binding={bm} feature={feat} model={model}\")\n lines.append(\"\")\n if all_msgs:\n lines.append(\"messages:\")\n for mid, role in all_msgs:\n lines.append(f\" {mid} role={role}\")\n lines.append(\"```\")\n lines.append(\"\")\n return lines\n\n\ndef _section_transcript(sess: dict, start_dt: Optional[datetime]) -> List[str]:\n turns = [iv for iv in sess.get(\"interactions\", []) if iv.get(\"type\") == \"TURN\"]\n if not turns:\n return []\n lines: List[str] = [\"## Transcript\", \"\"]\n for iv in turns:\n start_off = _fmt_offset(iv.get(\"start_ts\"), start_dt)\n dur = _fmt_duration_ms(iv.get(\"start_ts\"), iv.get(\"end_ts\"))\n lines.append(f\"**Interaction {_short(iv.get('id'))}** · {start_off} · {dur}\")\n for m in iv.get(\"messages\", []):\n role = m.get(\"role\") or m.get(\"type\") or \"?\"\n lines.append(f\"> **{role}:** {_decode(m.get('text') or '')}\")\n lines.append(\"\")\n return lines\n\n\ndef _section_hierarchical_trace(sess: dict, start_dt: Optional[datetime],\n effective_end_iso: Optional[str],\n end_source: Optional[str]) -> List[str]:\n lines: List[str] = [\n \"## Complete hierarchical trace\",\n \"\",\n \"Notation: `+Xs` = offset in seconds from session start. \"\n \"UUIDs are truncated to 8 chars + `…` for readability; \"\n \"full forms are in the **ID reference** block above.\",\n \"\",\n \"```\",\n ]\n\n # Session header\n lines.append(f\"SESSION {_short(sess.get('id'))}\")\n start_iso = sess.get(\"start_ts\")\n if start_iso:\n lines.append(f\"│ Start +0.000s ({start_iso})\")\n\n end_iso_raw = sess.get(\"end_ts\")\n end_type = sess.get(\"end_type\") or None\n outcome_s = end_type or \"—\"\n if not end_type and not end_iso_raw:\n outcome_s = \"— (session end not yet materialized in STDM)\"\n display_end = end_iso_raw or effective_end_iso\n if display_end:\n label_suffix = f\" [{end_source}]\" if end_source else \"\"\n lines.append(\n f\"│ End {_fmt_offset(display_end, start_dt)} ({display_end}){label_suffix} \"\n f\"outcome={outcome_s}\"\n )\n else:\n lines.append(f\"│ End — (session still open) outcome={outcome_s}\")\n lines.append(\"│\")\n\n interactions = sess.get(\"interactions\", [])\n for iv_idx, iv in enumerate(interactions):\n is_last_iv = iv_idx == len(interactions) - 1\n iv_branch = \"└──\" if is_last_iv else \"├──\"\n iv_cont = \" \" if is_last_iv else \"│ \"\n\n iv_start_off = _fmt_offset(iv.get(\"start_ts\"), start_dt)\n iv_end_off = _fmt_offset(iv.get(\"end_ts\"), start_dt)\n iv_dur = _fmt_duration_ms(iv.get(\"start_ts\"), iv.get(\"end_ts\"))\n iv_type = iv.get(\"type\") or \"?\"\n lines.append(\n f\"{iv_branch} {iv_type} {_short(iv.get('id'))} \"\n f\"{iv_start_off} + {iv_dur} = {iv_end_off}\"\n )\n\n topic = iv.get(\"topic\")\n if topic:\n lines.append(f\"{iv_cont}├── TOPIC: {topic}\")\n\n for m in iv.get(\"messages\", []):\n role = m.get(\"role\") or m.get(\"type\") or \"?\"\n m_off = _fmt_offset(m.get(\"ts\"), start_dt) if m.get(\"ts\") else \"—\"\n lines.append(f\"{iv_cont}├── {role} message {_short(m.get('message_id'))} ts={m_off}\")\n lines.append(f\"{iv_cont}│ └── text: \\\"{_truncate(m.get('text'), 100)}\\\"\")\n\n steps = iv.get(\"steps\", [])\n tsbound = iv.get(\"timestamp_bound_gateway_calls\", [])\n tsb_by_step: Dict[str, List[dict]] = {}\n tsb_interaction_level: List[dict] = []\n for tb in tsbound:\n bsid = tb.get(\"bound_to_step_id\")\n if bsid:\n tsb_by_step.setdefault(bsid, []).append(tb)\n else:\n tsb_interaction_level.append(tb)\n\n remaining_groups: List[str] = []\n if steps:\n remaining_groups.append(\"steps\")\n if tsb_interaction_level:\n remaining_groups.append(\"ts_il\")\n\n for grp_idx, grp in enumerate(remaining_groups):\n grp_is_last = grp_idx == len(remaining_groups) - 1\n grp_branch = \"└──\" if grp_is_last else \"├──\"\n grp_cont = \" \" if grp_is_last else \"│ \"\n if grp == \"steps\":\n lines.append(f\"{iv_cont}{grp_branch} STEPS:\")\n for st_idx, st in enumerate(steps):\n lines.extend(_render_step(\n st, st_idx, len(steps), iv_cont, grp_cont,\n start_dt, tsb_by_step,\n ))\n else: # grp == \"ts_il\"\n lines.append(f\"{iv_cont}{grp_branch} INTERACTION-LEVEL TIMESTAMP-BOUND GATEWAY CALLS:\")\n for tb_idx, tb in enumerate(tsb_interaction_level):\n tb_is_last = tb_idx == len(tsb_interaction_level) - 1\n tb_branch = \"└──\" if tb_is_last else \"├──\"\n lines.append(\n f\"{iv_cont}{grp_cont}{tb_branch} \"\n f\"gateway_request [timestamp_window, interaction-level] \"\n f\"{_short(tb.get('gateway_request_id'))} \"\n f\"feature={tb.get('feature') or '—'} \"\n f\"model={tb.get('model') or '—'}\"\n )\n\n if not is_last_iv:\n lines.append(\"│\")\n\n # Unbound gateway calls (session root level)\n ub = sess.get(\"unbound_gateway_calls\", [])\n if ub:\n lines.append(\"\")\n lines.append(\n f\"UNBOUND GATEWAY CALLS ({len(ub)}) — neither declared chain \"\n \"nor timestamp window matched\"\n )\n for i, g in enumerate(ub):\n branch = \"└──\" if i == len(ub) - 1 else \"├──\"\n lines.append(\n f\"{branch} {_short(g.get('gateway_request_id'))} \"\n f\"feature={g.get('feature') or '—'} model={g.get('model') or '—'}\"\n )\n\n lines.append(\"```\")\n lines.append(\"\")\n return lines\n\n\ndef _render_step(st: dict, st_idx: int, n_steps: int,\n iv_cont: str, grp_cont: str,\n start_dt: Optional[datetime],\n tsb_by_step: Dict[str, List[dict]]) -> List[str]:\n \"\"\"Render one step + its nested generation / gateway_request / ts-bound GWs.\"\"\"\n st_is_last = st_idx == n_steps - 1\n st_branch = \"└──\" if st_is_last else \"├──\"\n st_cont = \" \" if st_is_last else \"│ \"\n st_start_off = _fmt_offset(st.get(\"start_ts\"), start_dt)\n st_end_off = _fmt_offset(st.get(\"end_ts\"), start_dt)\n st_dur = _fmt_duration_ms(st.get(\"start_ts\"), st.get(\"end_ts\"))\n\n # Show the bound LLM model alongside the step name when known.\n # `model_name` is mirrored from the bound gateway_request by\n # assemble_dc; None when the declared chain didn't reach or when STDM\n # dropped writes (the `gateway_requests_dropped_by_stdm` shape).\n name_cell = st.get(\"name\") or \"—\"\n model_name = st.get(\"model_name\")\n if model_name:\n name_cell = f\"{name_cell} · {model_name}\"\n\n lines = [\n f\"{iv_cont}{grp_cont}{st_branch} {st.get('type') or '?'} {_short(st.get('id'))} \"\n f\"name={name_cell} {st_start_off} + {st_dur} = {st_end_off}\"\n ]\n\n # Step children: error, generation (+ trust signals), gateway_request, collision, bound GWs.\n step_kids: List[tuple] = []\n if st.get(\"error_text\"):\n step_kids.append((\"error\", None))\n gen = st.get(\"generation\")\n if gen:\n step_kids.append((\"gen\", gen))\n gw = st.get(\"gateway_request\")\n if gw:\n step_kids.append((\"gw\", gw))\n if st.get(\"gateway_request_collision\"):\n step_kids.append((\"collision\", None))\n for tb in tsb_by_step.get(st.get(\"id\", \"\"), []):\n step_kids.append((\"tsb\", tb))\n\n prefix = f\"{iv_cont}{grp_cont}{st_cont}\"\n for k_idx, (ktype, kval) in enumerate(step_kids):\n k_is_last = k_idx == len(step_kids) - 1\n k_branch = \"└──\" if k_is_last else \"├──\"\n k_cont = \" \" if k_is_last else \"│ \"\n\n if ktype == \"error\":\n lines.append(f\"{prefix}{k_branch} ERROR: {st['error_text']}\")\n elif ktype == \"collision\":\n lines.append(\n f\"{prefix}{k_branch} ⚠ gateway_request_collision: \"\n \"earlier step claimed the declared GW\"\n )\n elif ktype == \"gen\":\n lines.extend(_render_generation(kval, prefix, k_branch, k_cont,\n step_name=st.get(\"name\")))\n elif ktype == \"gw\":\n lines.extend(_render_gw_declared(kval, prefix, k_branch, k_cont))\n elif ktype == \"tsb\":\n tb = kval\n lines.append(\n f\"{prefix}{k_branch} gateway_request [timestamp_window] \"\n f\"{_short(tb.get('gateway_request_id'))} \"\n f\"feature={tb.get('feature') or '—'} \"\n f\"model={tb.get('model') or '—'}\"\n )\n return lines\n\n\n_ROLE_LABELS = {\n \"ReactTopicPrompt\": \"topic-classification output\",\n \"ReactInitialPrompt\": \"ReAct planner step\",\n \"ReactValidationPrompt\": \"ReAct validator\",\n \"ReactFormatSurfaceResponsePrompt\": \"response formatter\",\n}\n\n\ndef _role_label_for(step_name: Optional[str]) -> Optional[str]:\n if not step_name:\n return None\n for key, label in _ROLE_LABELS.items():\n if key in step_name:\n return label\n return None\n\n\ndef _parse_finish_reason(response_parameters: Optional[str]) -> Optional[str]:\n \"\"\"Pull finish_reason out of the HTML-escaped JSON in responseParameters__c.\n Shape: `{"finish_reason":"\"stop\"",...}`.\"\"\"\n if not response_parameters:\n return None\n try:\n decoded = html.unescape(response_parameters)\n parsed = json.loads(decoded)\n except (ValueError, TypeError):\n return None\n raw = parsed.get(\"finish_reason\") if isinstance(parsed, dict) else None\n if not isinstance(raw, str):\n return None\n # value often comes wrapped in escaped quotes: `\\\"stop\\\"` → `stop`\n return raw.replace(\"\\\\\", \"\").strip('\"').strip()\n\n\ndef _decoded_line(response_text: Optional[str]) -> str:\n \"\"\"Render `decoded:` content. Detect tool-call JSON and summarize.\n responseText__c is HTML-escaped (" etc.) in the wire format — unescape\n before trying to parse as JSON.\"\"\"\n if not response_text:\n return \"—\"\n candidate = html.unescape(response_text).strip()\n if candidate.startswith(\"{\") and '\"toolInvocations\"' in candidate:\n try:\n obj = json.loads(candidate)\n tools = obj.get(\"toolInvocations\") or []\n content = (obj.get(\"content\") or \"\").strip()\n if tools:\n first = tools[0].get(\"function\") or {}\n name = first.get(\"name\") or \"?\"\n args_raw = first.get(\"arguments\") or \"\"\n arg_summary = \"\"\n try:\n args_obj = json.loads(args_raw) if isinstance(args_raw, str) else args_raw\n if isinstance(args_obj, dict) and args_obj:\n k, v = next(iter(args_obj.items()))\n v_str = str(v)\n if len(v_str) > 60:\n v_str = v_str[:57] + \"…\"\n arg_summary = f'{k}=\"{v_str}\"'\n except (ValueError, TypeError):\n pass\n prefix = \"no user text\" if not content else f'\"{_truncate(content, 60)}\"'\n n = len(tools)\n call_word = \"tool call\" if n == 1 else \"tool calls\"\n return f\"{prefix}; {n} {call_word} → {name}({arg_summary})\"\n except (ValueError, TypeError):\n pass\n return f'\"{_truncate(response_text, 140)}\"'\n\n\ndef _float_or_none(v) -> Optional[float]:\n try:\n return float(v)\n except (ValueError, TypeError):\n return None\n\n\ndef _trust_line(g: dict) -> str:\n \"\"\"Summarize trust signals. TOXICITY sub-categories (parented on the\n quality row) carry a dedicated `safety_score` category in the 0-1 range\n (1.0 = clean). Non-`safety_score` sub-categories are the 8 hazard axes\n (profanity, hate, violence, …); report the max of those when a detection\n fires so the reader sees which axis tripped.\n Non-TOXICITY detectors (InstructionAdherence, etc.) are parented directly\n on the generation and render as textual verdicts.\"\"\"\n quality = g.get(\"quality\") or []\n cats = g.get(\"categories\") or []\n if not quality and not cats:\n return \"— (no quality/category rows)\"\n parts: List[str] = []\n for q in quality:\n subs = q.get(\"_toxicity_subcategories\") or []\n detected = str(q.get(\"isToxicityDetected__c\", \"\")).lower() == \"true\"\n safety: Optional[float] = None\n hazard_subs: List[tuple] = []\n for s in subs:\n if (s.get(\"detectorType__c\") or \"\").upper() != \"TOXICITY\":\n continue\n name = s.get(\"category__c\") or \"\"\n val = _float_or_none(s.get(\"value__c\"))\n if val is None:\n continue\n if name == \"safety_score\":\n safety = val\n else:\n hazard_subs.append((name, val))\n status = \"detected\" if detected else \"clean\"\n safety_str = f\"{safety:.2f}\" if safety is not None else \"—\"\n if not detected and hazard_subs and all(v == 0 for _, v in hazard_subs):\n detail = f\" — all {len(hazard_subs)} sub-categories 0.00\"\n elif hazard_subs:\n top_name, top_val = max(hazard_subs, key=lambda kv: kv[1])\n detail = f\" — max {top_name}={top_val:.2f}\"\n else:\n detail = \"\"\n parts.append(f\"TOXICITY safety_score={safety_str} ({status}{detail})\")\n # Non-TOXICITY detectors (generation-direct).\n for c in cats:\n dtype = (c.get(\"detectorType__c\") or \"?\").strip()\n if dtype.upper() == \"TOXICITY\":\n continue\n val = c.get(\"value__c\")\n parts.append(f\"{dtype}: {_truncate(val, 80)}\")\n return \"; \".join(parts) if parts else \"—\"\n\n\ndef _render_generation(g: dict, prefix: str, k_branch: str, k_cont: str,\n *, step_name: Optional[str] = None) -> List[str]:\n \"\"\"Generation node: 3 fixed children (decoded / finish_reason+masked / trust).\n Header is naked (id only); `response_id` and `feature` live in the ID reference\n block / on the sibling gateway_request line.\"\"\"\n lines = [\n f\"{prefix}{k_branch} generation {_short(g.get('generation_id'))}\"\n ]\n role = _role_label_for(step_name)\n decoded = _decoded_line(g.get(\"response_text\"))\n if role:\n decoded_line = f\"decoded: {decoded} ({role})\"\n else:\n decoded_line = f\"decoded: {decoded}\"\n masked = g.get(\"masked_response_text\")\n masked_disp = _truncate(masked, 60) if masked else \"—\"\n finish_reason = _parse_finish_reason(g.get(\"response_parameters\")) or \"—\"\n gprefix = f\"{prefix}{k_cont}\"\n lines.append(f\"{gprefix}├── {decoded_line}\")\n lines.append(f\"{gprefix}├── finish_reason={finish_reason} masked={masked_disp}\")\n lines.append(f\"{gprefix}└── trust: {_trust_line(g)}\")\n return lines\n\n\ndef _render_gw_declared(g: dict, prefix: str, k_branch: str, k_cont: str) -> List[str]:\n tokens = (\n f\"prompt={int(g.get('prompt_tokens') or 0)} \"\n f\"completion={int(g.get('completion_tokens') or 0)} \"\n f\"total={int(g.get('total_tokens') or 0)}\"\n )\n tags = g.get(\"tags\") or []\n md = g.get(\"metadata\") or []\n recs = g.get(\"records\") or []\n llm = g.get(\"llm\") or []\n audit_line = (\n f\"audit: tags={len(tags)} metadata={len(md)} records={len(recs)} llm={len(llm)}\"\n if (tags or md or recs or llm)\n else \"audit: (none)\"\n )\n return [\n f\"{prefix}{k_branch} gateway_request [declared] \"\n f\"{_short(g.get('gateway_request_id'))} \"\n f\"feature={g.get('feature') or '—'} \"\n f\"model={g.get('model') or '—'} \"\n f\"provider={g.get('provider') or '—'}\",\n f\"{prefix}{k_cont}├── tokens: {tokens}\",\n f\"{prefix}{k_cont}└── {audit_line}\",\n ]\n\n\ndef _section_per_turn_summary(sess: dict, start_dt: Optional[datetime]) -> List[str]:\n lines: List[str] = [\n \"## Per-turn summary\",\n \"\",\n \"| Interaction | Type | Start offset | Duration | Steps | \"\n \"GW declared | GW ts_window | USER → AGENT |\",\n \"|---|---|---|---|---|---|---|---|\",\n ]\n for iv in sess.get(\"interactions\", []):\n iv_type = iv.get(\"type\") or \"?\"\n start_off = _fmt_offset(iv.get(\"start_ts\"), start_dt)\n dur = _fmt_duration_ms(iv.get(\"start_ts\"), iv.get(\"end_ts\"))\n step_count = len(iv.get(\"steps\", []))\n declared_gws = sum(\n 1 for st in iv.get(\"steps\", [])\n if st.get(\"gateway_request\")\n and st[\"gateway_request\"].get(\"binding_method\") == \"declared\"\n )\n tsw_gws = len(iv.get(\"timestamp_bound_gateway_calls\", []))\n user_msg = next((m for m in iv.get(\"messages\", []) if m.get(\"role\") == \"USER\"), None)\n agent_msg = next((m for m in iv.get(\"messages\", []) if m.get(\"role\") == \"AGENT\"), None)\n ut = _truncate(user_msg.get(\"text\") if user_msg else None, 40)\n at = _truncate(agent_msg.get(\"text\") if agent_msg else None, 40)\n lines.append(\n f\"| `{_short(iv.get('id'))}` | {iv_type} | {start_off} | {dur} | \"\n f\"{step_count} | {declared_gws} | {tsw_gws} | {ut} → {at} |\"\n )\n lines.append(\"\")\n return lines\n\n\n# ---- visual analysis (mermaid) -------------------------------------------\n\n# Chars that break mermaid label parsing when bare; quote-wrap the string\n# when any of these show up. Underscore+digit tokens in topic strings are\n# legal inside mermaid node labels — only syntax-significant chars need\n# escaping here.\n_MERMAID_LABEL_SPECIALS = set(',()\u003c>:\"#')\n\n\ndef _escape_mermaid_label(s: str) -> str:\n if not s:\n return '\"\"'\n if any(ch in _MERMAID_LABEL_SPECIALS for ch in s):\n return '\"' + s.replace('\"', '\\\\\"') + '\"'\n return s\n\n\ndef _sequence_msg(s: str) -> str:\n \"\"\"Sanitize free-form text for the right-hand side of a sequenceDiagram\n arrow. Mermaid parses `A->>B: \u003ctext>` up to newline — the text itself is\n unquoted, so wrapping in `\"...\"` (as node-label escape does) renders the\n quotes literally. We just strip newlines and escape the one char that\n ends the message field (semicolon is the statement terminator).\"\"\"\n if not s:\n return \"—\"\n return s.replace(\"\\n\", \" \").replace(\";\", \",\").strip()\n\n\ndef _flowchart_edge_label(s: str) -> str:\n \"\"\"Sanitize free-form text for a flowchart edge label `A -->|label| B`.\n The delimiter is the pipe itself, so a literal `|` in the label shatters\n the parser. Square brackets also clash with node-shape syntax. Strip/\n substitute these; other specials (commas, colons, parens) are fine\n inside quoted labels, which `_escape_mermaid_label` handles.\"\"\"\n if not s:\n return \"\"\n cleaned = s.replace(\"|\", \"/\").replace(\"[\", \"(\").replace(\"]\", \")\")\n return _escape_mermaid_label(cleaned)\n\n\ndef _step_display_name(st: dict) -> str:\n \"\"\"Short, mermaid-safe row label for a step.\n\n - Strip `AiCopilot__` prefix on LLM prompts.\n - For ACTION_STEP names shaped `Topic_xxx.AGNT_Action_yyy`, keep the\n action-name portion after the last `.` so the row reads as the\n invoked action, not the topic.\n \"\"\"\n name = st.get(\"name\") or st.get(\"type\") or \"?\"\n if name.startswith(\"AiCopilot__\"):\n name = name[len(\"AiCopilot__\"):]\n if st.get(\"type\") == \"ACTION_STEP\" and \".\" in name:\n name = name.rsplit(\".\", 1)[-1]\n # Gantt task names cannot contain colons (task syntax is `label : id, start, dur`).\n return name.replace(\":\", \"·\")\n\n\ndef _iter_turns(sess: dict) -> List[dict]:\n return [iv for iv in sess.get(\"interactions\", []) if iv.get(\"type\") == \"TURN\"]\n\n\ndef _mermaid_gantt(\n sess: dict,\n start_dt: Optional[datetime],\n llm_calls: Optional[List[dict]] = None,\n) -> List[str]:\n \"\"\"Gantt chart — wall-clock timeline.\n\n Rows per turn (ACTION steps tagged :crit). When `llm_calls` is\n non-empty, a trailing \"LLM calls\" section renders per-call rows\n tagged by region-class (:active = cross_region, :done = same-\n region, plain = unknown). Mermaid's built-in 3-class palette is\n enough signal for v1; full per-region color would need a classDef\n block (deferred — see plan OOS-2.A).\n\n Kill-criterion: \u003c3 total rows after dropping zero-duration steps\n AND no LLM-call rows → return []. If the step set is thin but the\n call set is non-empty, we still render (the call rows are the\n whole point of the overlay).\n \"\"\"\n if start_dt is None:\n return []\n rows: List[tuple] = [] # (section_label, row_label, start_ms, end_ms, is_action)\n turns = _iter_turns(sess)\n for iv in sess.get(\"interactions\", []):\n if iv.get(\"type\") != \"TURN\":\n continue\n topic = iv.get(\"topic\") or \"(no topic)\"\n section = f\"Turn {turns.index(iv) + 1} ({topic})\"\n for st in iv.get(\"steps\", []):\n if st.get(\"type\") == \"SESSION_END\":\n continue\n s_dt = _parse_iso(st.get(\"start_ts\"))\n e_dt = _parse_iso(st.get(\"end_ts\"))\n if s_dt is None or e_dt is None:\n continue\n s_ms = int((s_dt - start_dt).total_seconds() * 1000)\n e_ms = int((e_dt - start_dt).total_seconds() * 1000)\n if e_ms \u003c= s_ms:\n continue # skip zero-duration (e.g. TOPIC_STEP markers)\n rows.append((section, _step_display_name(st), s_ms, e_ms,\n st.get(\"type\") == \"ACTION_STEP\"))\n\n # LLM-call rows — one per gateway call, derived from _llm_calls.json.\n # Rendered in a trailing section so they don't interleave with step\n # rows (mermaid sections don't sort across boundaries).\n call_rows: List[tuple] = [] # (row_label, start_ms, end_ms, class_tag)\n for call in (llm_calls or []):\n t_iso = call.get(\"_time\")\n dur_ms = call.get(\"duration_ms\")\n if not t_iso or dur_ms is None:\n continue\n t_dt = _parse_iso(t_iso)\n if t_dt is None:\n continue\n s_ms = int((t_dt - start_dt).total_seconds() * 1000)\n e_ms = s_ms + int(dur_ms)\n if e_ms \u003c= s_ms:\n continue\n # class_tag: cross_region=True → :active (mermaid's yellow),\n # cross_region=False → :done (grey), unknown/None → no tag\n # (default blue). Operators can scan for the yellow bars as\n # the cross-region outliers.\n cr = call.get(\"cross_region\")\n if cr is True:\n class_tag = \":active, \"\n elif cr is False:\n class_tag = \":done, \"\n else:\n class_tag = \":\"\n model = call.get(\"model_name\") or \"call\"\n region = call.get(\"routing_decision\") or \"—\"\n label = _escape_mermaid_label(f\"{model} [{region}]\")\n call_rows.append((label, s_ms, e_ms, class_tag))\n\n if len(rows) \u003c 3 and not call_rows:\n return []\n\n # axisFormat: `%M:%S` renders as minutes:seconds via d3. Our offsets\n # are ms-from-start passed through `dateFormat x` (unix epoch); d3\n # treats them as Jan 1 1970 timestamps, so for any session under\n # 1 hour this reads cleanly as m:ss elapsed. `%L` (ms-of-second)\n # always rendered 000 because ticks land at second boundaries.\n # The leading `section anchor` + 1ms milestone forces the x-axis to\n # start at 00:00 — mermaid derives axis min from the smallest task\n # timestamp, and without the anchor that's the first step's offset\n # (~5s into a typical session), making the axis misleadingly slide.\n lines = [\n \"```mermaid\",\n \"gantt\",\n \" title Session timeline (m:ss from start)\",\n \" dateFormat x\",\n \" axisFormat %M:%S\",\n \" section Session start\",\n \" t0 :milestone, 0, 0\",\n ]\n current_section: Optional[str] = None\n for section, label, s_ms, e_ms, is_action in rows:\n if section != current_section:\n lines.append(f\" section {section}\")\n current_section = section\n # Mermaid gantt task: `\u003cname> :[tag,] \u003cstart>, \u003cend>` — single colon,\n # tag is the first comma-separated field after it. Two colons (the\n # `:crit:` shape) is a parse error.\n prefix = \":crit, \" if is_action else \":\"\n lines.append(f\" {label} {prefix}{s_ms}, {e_ms}\")\n if call_rows:\n lines.append(\" section LLM calls\")\n for label, s_ms, e_ms, class_tag in call_rows:\n lines.append(f\" {label} {class_tag}{s_ms}, {e_ms}\")\n lines.append(\"```\")\n lines.append(\"\")\n return lines\n\n\ndef _action_short_name(st: dict) -> str:\n name = st.get(\"name\") or \"\"\n if \".\" in name:\n name = name.rsplit(\".\", 1)[-1]\n return name or \"action\"\n\n\ndef _mermaid_sequence_per_turn(iv: dict, start_dt: Optional[datetime],\n turn_no: int) -> List[str]:\n steps = iv.get(\"steps\", [])\n has_error = any(st.get(\"error_text\") for st in steps)\n action_steps = [st for st in steps if st.get(\"type\") == \"ACTION_STEP\"]\n if not has_error and len(action_steps) \u003c 2:\n return []\n\n topic = iv.get(\"topic\") or \"(no topic)\"\n heading = f\"### Turn {turn_no} ({topic}) — control flow\"\n lines: List[str] = [heading, \"\", \"```mermaid\", \"sequenceDiagram\"]\n\n # Participants: U, P, L are always present; actions get A1, A2, …\n lines.append(\" participant U as USER\")\n lines.append(\" participant P as Planner\")\n lines.append(\" participant L as LLM Gateway\")\n action_alias: Dict[str, str] = {} # step id → alias\n for i, st in enumerate(action_steps, start=1):\n alias = f\"A{i}\"\n action_alias[st.get(\"id\") or f\"_a{i}\"] = alias\n label = _escape_mermaid_label(_action_short_name(st))\n lines.append(f\" participant {alias} as {label}\")\n\n # User → Planner (utterance).\n user_msg = next((m for m in iv.get(\"messages\", []) if m.get(\"role\") == \"USER\"), None)\n user_text = _truncate(user_msg.get(\"text\") if user_msg else None, 40)\n if user_msg:\n lines.append(f\" U->>P: {_sequence_msg(user_text)}\")\n\n # Walk steps in order. LLM → L, ACTION → A\u003cn>, errors → Note over.\n for st in steps:\n stype = st.get(\"type\")\n if stype == \"LLM_STEP\":\n label = _step_display_name(st)\n gw = st.get(\"gateway_request\") or {}\n model = gw.get(\"model\")\n suffix = f\" ({model})\" if model else \"\"\n lines.append(f\" P->>L: {_sequence_msg(label + suffix)}\")\n dur = _fmt_duration_ms(st.get(\"start_ts\"), st.get(\"end_ts\"))\n if st.get(\"error_text\"):\n lines.append(f\" Note over L: ERROR in {dur}\")\n else:\n lines.append(f\" L-->>P: ok ({dur})\")\n elif stype == \"ACTION_STEP\":\n alias = action_alias.get(st.get(\"id\") or \"\", \"A?\")\n dur = _fmt_duration_ms(st.get(\"start_ts\"), st.get(\"end_ts\"))\n lines.append(f\" P->>{alias}: invoke\")\n lines.append(f\" {alias}-->>P: result ({dur})\")\n\n # Agent reply (if any).\n agent_msg = next((m for m in iv.get(\"messages\", []) if m.get(\"role\") == \"AGENT\"), None)\n if agent_msg:\n agent_text = _truncate(agent_msg.get(\"text\"), 40)\n lines.append(f\" P-->>U: {_sequence_msg(agent_text)}\")\n\n lines.append(\"```\")\n lines.append(\"\")\n return lines\n\n\ndef _mermaid_topic_flowchart(sess: dict) -> List[str]:\n turns = _iter_turns(sess)\n if not turns:\n return []\n # Order-preserving unique topics.\n topic_ids: Dict[str, str] = {}\n for iv in turns:\n t = iv.get(\"topic\") or \"(no topic)\"\n if t not in topic_ids:\n topic_ids[t] = f\"T{len(topic_ids) + 1}\"\n if len(topic_ids) == len(turns):\n return [] # linear — no repeats, skip\n\n lines: List[str] = [\"```mermaid\", \"flowchart LR\"]\n # Node declarations.\n for topic, node_id in topic_ids.items():\n lines.append(f\" {node_id}[{_escape_mermaid_label(topic)}]\")\n\n # Edges: one per turn transition, labelled by the user utterance that\n # drove into that turn. The first turn has no predecessor so we just\n # declare the node; edges start from turn 2.\n prev_topic = turns[0].get(\"topic\") or \"(no topic)\"\n for iv in turns[1:]:\n cur_topic = iv.get(\"topic\") or \"(no topic)\"\n user_msg = next((m for m in iv.get(\"messages\", []) if m.get(\"role\") == \"USER\"), None)\n utter = _truncate(user_msg.get(\"text\") if user_msg else None, 30)\n src = topic_ids[prev_topic]\n dst = topic_ids[cur_topic]\n label = _flowchart_edge_label(utter) if utter and utter != \"—\" else \"\"\n arrow = f\" {src} -->|{label}| {dst}\" if label else f\" {src} --> {dst}\"\n lines.append(arrow)\n prev_topic = cur_topic\n\n # Error class for topics whose turn ended with an error_text.\n errored_topic_ids: List[str] = []\n for iv in turns:\n if any(st.get(\"error_text\") for st in iv.get(\"steps\", [])):\n tid = topic_ids[iv.get(\"topic\") or \"(no topic)\"]\n if tid not in errored_topic_ids:\n errored_topic_ids.append(tid)\n if errored_topic_ids:\n lines.append(f\" classDef err fill:#fee,stroke:#c00\")\n for tid in errored_topic_ids:\n lines.append(f\" class {tid} err\")\n\n lines.append(\"```\")\n lines.append(\"\")\n return lines\n\n\ndef _mermaid_token_pie(sess: dict) -> List[str]:\n buckets: Dict[str, int] = {}\n total = 0\n for iv in sess.get(\"interactions\", []):\n for st in iv.get(\"steps\", []):\n gw = st.get(\"gateway_request\") or {}\n p = int(gw.get(\"prompt_tokens\") or 0)\n c = int(gw.get(\"completion_tokens\") or 0)\n tok = p + c\n if tok \u003c= 0:\n continue\n role = _role_label_for(st.get(\"name\")) or \"other\"\n buckets[role] = buckets.get(role, 0) + tok\n total += tok\n if total \u003c 1000 or not buckets:\n return []\n\n lines = [\n \"```mermaid\",\n \"pie showData\",\n f\" title Prompt-role token attribution (total = {total:,})\",\n ]\n # Largest slice first reads better in most renderers.\n for role, n in sorted(buckets.items(), key=lambda kv: -kv[1]):\n lines.append(f' \"{role}\" : {n}')\n lines.append(\"```\")\n lines.append(\"\")\n return lines\n\n\ndef _section_visual_analysis(\n sess: dict,\n start_dt: Optional[datetime],\n llm_calls: Optional[List[dict]] = None,\n) -> List[str]:\n blocks: List[List[str]] = []\n gantt = _mermaid_gantt(sess, start_dt, llm_calls=llm_calls)\n if gantt:\n blocks.append(gantt)\n # Per-turn sequence diagrams (at most one per turn; most turns skip).\n turns = _iter_turns(sess)\n for idx, iv in enumerate(turns, start=1):\n seq = _mermaid_sequence_per_turn(iv, start_dt, idx)\n if seq:\n blocks.append(seq)\n flow = _mermaid_topic_flowchart(sess)\n if flow:\n blocks.append(flow)\n pie = _mermaid_token_pie(sess)\n if pie:\n blocks.append(pie)\n\n if not blocks:\n return []\n lines: List[str] = [\"## Visual analysis\", \"\"]\n for b in blocks:\n lines.extend(b)\n return lines\n\n\ndef _section_session_counts(sess: dict) -> List[str]:\n c = sess.get(\"counts\", {})\n st_by_type = c.get(\"steps_by_type\", {})\n gwb = c.get(\"gw_binding\", {})\n lines: List[str] = [\n \"## Session counts\",\n \"\",\n \"| metric | value |\",\n \"|---|---|\",\n f\"| interactions | {c.get('interactions_total', 0)} |\",\n f\"| steps | {c.get('steps_total', 0)} |\",\n f\"| llm_steps | {st_by_type.get('LLM_STEP', 0)} |\",\n f\"| action_steps | {st_by_type.get('ACTION_STEP', 0)} |\",\n f\"| gateway_requests | {c.get('gateway_requests', 0)} |\",\n f\"| gateway_responses | {c.get('gateway_responses', 0)} |\",\n f\"| 1:1 invariant | {'✓' if c.get('audit_chain_1to1_ok') else '✗'} |\",\n f\"| gw declared | {gwb.get('declared', 0)} |\",\n f\"| gw timestamp_window | {gwb.get('timestamp_window', 0)} |\",\n f\"| gw unbound | {gwb.get('unbound', 0)} |\",\n ]\n if gwb.get(\"declared_collisions\"):\n lines.append(f\"| ⚠ declared_collisions | {gwb['declared_collisions']} |\")\n lines.append(\"\")\n return lines\n\n\ndef _section_empties_diagnostics(manifest: dict) -> List[str]:\n \"\"\"Operator-actionable: lift `_unavailable_reason` verbatim from the manifest.\"\"\"\n queries = manifest.get(\"queries\", []) if manifest else []\n empties = [\n q for q in queries\n if q.get(\"rows\") == 0 and q.get(\"_unavailable_reason\")\n ]\n if not empties:\n return []\n lines: List[str] = [\n \"## Empties diagnostics\",\n \"\",\n \"DMOs that returned zero rows, with the reason lifted verbatim from the manifest:\",\n \"\",\n \"| DMO | Reason |\",\n \"|---|---|\",\n ]\n for q in empties:\n name = q.get(\"name\") or \"?\"\n # Pipe characters in reason text would break markdown tables.\n reason = str(q.get(\"_unavailable_reason\") or \"\").replace(\"|\", \"\\\\|\")\n lines.append(f\"| {name} | {reason} |\")\n lines.append(\"\")\n return lines\n\n\ndef _section_catalog(tree: dict) -> List[str]:\n catalog = tree.get(\"catalog\", {}) or {}\n agents = \", \".join(catalog.get(\"agents_observed\", []) or []) or \"—\"\n return [\n \"## Catalog (session-filtered)\",\n \"\",\n f\"- TagDefinitions: {len(catalog.get('tag_definitions', []) or [])}\",\n f\"- TagDefinitionAssociations: \"\n f\"{len(catalog.get('tag_definition_associations', []) or [])} (agents: {agents})\",\n f\"- Tags: {len(catalog.get('tags', []) or [])}\",\n \"\",\n ]\n\n\n# ---- top-level render + entry points -------------------------------------\n\ndef render(\n tree: dict,\n manifest: Optional[dict] = None,\n session_dir: Optional[Path] = None,\n *,\n show_prompts: bool = False,\n) -> str:\n \"\"\"Produce the summary markdown for a single session.\n\n Branches on tree shape:\n - gateway-direct tree (``_source == \"gateway_direct\"``) → identity +\n lag banner + gateway-chain table + per-call detail; skips the\n Interaction-dependent sections.\n - minimal tree (session-not-found) → short markdown with just identity.\n - full tree → multi-section summary (see SKILL.md for the full list).\n\n ``session_dir`` is reserved for callers that want the renderer to look\n up artifacts beside the tree on disk. The standalone d360 skill produces\n no runtime-telemetry rollups — DC alone doesn't expose per-turn LLM\n latency in a useful form — so when ``None`` (the test-friendly default),\n the gantt simply draws without the LLM-call overlay.\n\n ``show_prompts`` (opt-in): when True, the full-tree branch appends\n a \"Planner LLM calls\" section with full prompt + response text per\n LLM call. Default False — the section can add hundreds of KB to the\n summary on multi-turn sessions.\n\n Refuses incompatible tree schema versions (see `_assert_schema_version`).\n \"\"\"\n _assert_schema_version(tree)\n\n # Gateway-direct branch — session resolved but STDM hierarchy hasn't\n # materialized yet. Handled before the has_interactions check because\n # the gateway-direct tree does set `session.interactions = []`.\n if tree.get(\"_source\") == \"gateway_direct\":\n return _render_gateway_direct(tree, manifest, show_prompts=show_prompts)\n\n sess = tree.get(\"session\") or {}\n sid = sess.get(\"id\") or \"\u003cunknown>\"\n has_interactions = \"interactions\" in sess\n\n # Minimal-tree early return.\n if not has_interactions:\n return _render_minimal(sid, sess)\n\n start_iso = sess.get(\"start_ts\")\n start_dt = _parse_iso(start_iso)\n effective_end_iso, end_source = _derive_session_end(sess)\n\n # The d360 skill produces no runtime-telemetry rollups — DC alone\n # doesn't expose per-turn LLM latency in a useful form. Visual\n # analysis falls back to its pre-rollup output.\n llm_calls: List[dict] = []\n\n lines: List[str] = [f\"# Session {sid}\", \"\"]\n lines.extend(_section_session_identity(sess, effective_end_iso, end_source))\n # VariableText__c bootstrap (channel-mode diagnostic).\n lines.extend(_section_session_bootstrap(\n sess.get(\"identity\") or {}, channel=sess.get(\"channel\"),\n ))\n lines.extend(_section_id_reference(sess))\n lines.extend(_section_transcript(sess, start_dt))\n lines.extend(_section_hierarchical_trace(sess, start_dt, effective_end_iso, end_source))\n lines.extend(_section_per_turn_summary(sess, start_dt))\n # Opt-in full prompt + response per LLM call. Off by\n # default — multi-turn sessions can produce hundreds of KB here.\n lines.extend(_section_planner_llm_calls(sess, show_prompts=show_prompts))\n lines.extend(_section_visual_analysis(sess, start_dt, llm_calls=llm_calls))\n lines.extend(_section_session_counts(sess))\n lines.extend(_section_empties_diagnostics(manifest or {}))\n lines.extend(_section_catalog(tree))\n return \"\\n\".join(lines) + \"\\n\"\n\n\ndef _render_minimal(sid: str, sess: dict) -> str:\n \"\"\"Short markdown for session-not-found minimal trees.\"\"\"\n shape = (sess.get(\"counts\") or {}).get(\"session_shape\", \"session_not_found\")\n lines = [\n f\"# Session {sid}\",\n \"\",\n \"## Session identity\",\n \"\",\n \"| Field | Value |\",\n \"|---|---|\",\n f\"| Session id | {sid} |\",\n f\"| Session shape | {shape} |\",\n \"\",\n \"_No interactions resolved in Data Cloud. Check the session id, or wait for \"\n \"STDM materialization._\",\n \"\",\n ]\n return \"\\n\".join(lines) + \"\\n\"\n\n\n# Display-only cap on `prompt_text` inside per-call detail. The raw JSON on\n# disk (dc.gateway_requests.json) is authoritative and never truncated — the\n# assembler stores the full prompt, and this slice only applies to markdown.\n_PROMPT_DISPLAY_CAP_BYTES = 65536\n\n\ndef _fmt_token_count(value) -> str:\n \"\"\"Tolerate ints, stringified ints, and None/''/NOT_SET.\"\"\"\n if value in (None, \"\", \"NOT_SET\"):\n return \"—\"\n return str(value)\n\n\ndef _render_gateway_direct(tree: dict, manifest: Optional[dict],\n *, show_prompts: bool = False) -> str:\n \"\"\"Render the STDM-hasn't-materialized-yet view.\n\n Sections:\n 1. Session identity (reused)\n 2. ID reference (reused; gracefully handles the empty\n interactions/participants on this path)\n 3. STDM lag banner (gateway-direct specific)\n 4. Gateway chain table (gateway-direct specific)\n 5. Per-call detail (gateway-direct specific)\n 6. Empties diagnostics (reused; reads manifest, not tree)\n 7. Catalog (reused; catalog may be empty on a\n fresh-session gateway-direct tree)\n\n Skipped: Transcript, Hierarchical trace, Per-turn summary, Session\n counts — all require Interaction rows that don't exist yet.\n\n ``show_prompts`` (default False): forwarded to per-call detail so the\n full prompt block only renders under ``--show-prompts``. The prior\n behavior unconditionally leaked prompts on this branch, contradicting\n the documented contract for ``dc._session_summary.md``.\n \"\"\"\n sess = tree.get(\"session\") or {}\n sid = sess.get(\"id\") or \"\u003cunknown>\"\n start_iso = sess.get(\"start_ts\")\n start_dt = _parse_iso(start_iso)\n # No derivation needed — sessions.end_ts is the only source on this path.\n effective_end_iso = sess.get(\"end_ts\")\n\n lines: List[str] = [f\"# Session {sid}\", \"\"]\n lines.extend(_section_session_identity(sess, effective_end_iso, None))\n # VariableText__c bootstrap (channel-mode diagnostic).\n lines.extend(_section_session_bootstrap(\n sess.get(\"identity\") or {}, channel=sess.get(\"channel\"),\n ))\n lines.extend(_section_id_reference(sess))\n lines.extend(_section_stdm_lag_banner())\n lines.extend(_section_gateway_chain_table(sess, start_dt))\n lines.extend(_section_gateway_per_call_detail(sess, show_prompts=show_prompts))\n lines.extend(_section_empties_diagnostics(manifest or {}))\n lines.extend(_section_catalog(tree))\n return \"\\n\".join(lines) + \"\\n\"\n\n\ndef _section_stdm_lag_banner() -> List[str]:\n return [\n \"## STDM materialization lag\",\n \"\",\n \"> **Note** STDM Interaction/Step/Message DMOs have not yet \"\n \"materialized for this session. The view below is the gateway chain \"\n \"harvested directly from Gateway DMOs (materialize in minutes). \"\n \"Re-run in 24–72h for the full hierarchical trace.\",\n \"\",\n ]\n\n\ndef _section_gateway_chain_table(sess: dict,\n start_dt: Optional[datetime]) -> List[str]:\n chain = sess.get(\"gateway_chain\") or []\n lines: List[str] = [\n \"## Gateway chain\",\n \"\",\n \"| # | Request ts | Model | Provider | Prompt template | \"\n \"Prompt tok | Completion tok | Total tok | Response ts |\",\n \"|---|---|---|---|---|---|---|---|---|\",\n ]\n for i, call in enumerate(chain, start=1):\n req_offset = _fmt_offset(call.get(\"timestamp\"), start_dt)\n resp_offset = _fmt_offset(\n (call.get(\"response\") or {}).get(\"timestamp\"), start_dt)\n model = call.get(\"model\") or \"—\"\n provider = call.get(\"provider\") or \"—\"\n template = call.get(\"prompt_template_dev_name\") or \"—\"\n prompt_tok = _fmt_token_count(call.get(\"prompt_tokens\"))\n completion_tok = _fmt_token_count(call.get(\"completion_tokens\"))\n total_tok = _fmt_token_count(call.get(\"total_tokens\"))\n lines.append(\n f\"| {i} | {req_offset} | {model} | {provider} | {template} | \"\n f\"{prompt_tok} | {completion_tok} | {total_tok} | {resp_offset} |\"\n )\n lines.append(\"\")\n return lines\n\n\ndef _pick_fence(text: str) -> str:\n \"\"\"Pick a backtick fence long enough to wrap `text` safely.\n\n CommonMark lets a fenced code block use any run of 3+ backticks; the\n closing fence must be at least as long as the opening. LLM prompts\n routinely contain triple-backticks inside tool-use examples, so a\n hardcoded ``` fence closes early and corrupts the rest of the doc.\n \"\"\"\n if not isinstance(text, str) or not text:\n return \"```\"\n longest = 0\n run = 0\n for ch in text:\n if ch == \"`\":\n run += 1\n if run > longest:\n longest = run\n else:\n run = 0\n return \"`\" * max(3, longest + 1)\n\n\ndef _capped_payload(text: Optional[str], note_source: str) -> tuple[str, bool, str]:\n \"\"\"Cap a payload string at `_PROMPT_DISPLAY_CAP_BYTES` for display.\n\n Returns ``(body, truncated, source_note)``. Byte-length check so\n multi-byte chars don't blow through the limit when the renderer emits\n UTF-8 text. Slicing happens on the encoded form, then decodes with\n ``errors=\"ignore\"`` so we never split a multi-byte char mid-sequence.\n `source_note` names the on-disk file with the authoritative full text.\n \"\"\"\n if not isinstance(text, str) or not text:\n return (\"(empty)\", False, note_source)\n encoded = text.encode(\"utf-8\")\n if len(encoded) \u003c= _PROMPT_DISPLAY_CAP_BYTES:\n return (text, False, note_source)\n body = encoded[:_PROMPT_DISPLAY_CAP_BYTES].decode(\"utf-8\", errors=\"ignore\")\n return (body, True, note_source)\n\n\ndef _render_call_detail_block(call: dict, idx: int, *,\n show_prompts: bool = False,\n show_response_text: bool = False) -> List[str]:\n \"\"\"Render one ``#### LLM call N — \u003cshort-id>`` block.\n\n Used by both:\n - the gateway-direct branch (``_section_gateway_per_call_detail``)\n - the full-tree opt-in section (``_section_planner_llm_calls``)\n\n ``call`` shape (subset of fields used here):\n gateway_request_id, model, provider, prompt_template_dev_name,\n prompt_tokens, completion_tokens, total_tokens, prompt_text,\n response (-> finish_reason), response_text (only on full-tree path).\n\n The prompt and response blocks are independently gated:\n - ``show_prompts`` controls the **Prompt** block.\n - ``show_response_text`` controls the **Response** block (full-tree\n only — gateway-direct chain entries don't carry response_text).\n Both default off so callers must opt in explicitly. The summary line\n (model/provider/template/tokens/finish_reason) always renders.\n \"\"\"\n gw_id = call.get(\"gateway_request_id\") or \"—\"\n short_id = _short(gw_id)\n lines = [f\"#### LLM call {idx} — {short_id}\", \"\"]\n finish_reason = (call.get(\"response\") or {}).get(\"finish_reason\") or \"—\"\n summary = (\n f\"- model={call.get('model') or '—'}\"\n f\" provider={call.get('provider') or '—'}\"\n f\" template={call.get('prompt_template_dev_name') or '—'}\"\n f\" prompt_tok={_fmt_token_count(call.get('prompt_tokens'))}\"\n f\" completion_tok={_fmt_token_count(call.get('completion_tokens'))}\"\n f\" total_tok={_fmt_token_count(call.get('total_tokens'))}\"\n f\" finish_reason={finish_reason}\"\n )\n lines.append(summary)\n lines.append(\"\")\n\n # Prompt block — gated by show_prompts. Default-off everywhere; the\n # summary file should never leak full prompt text without an explicit\n # --show-prompts opt-in (matches the doc contract in SKILL.md).\n if show_prompts:\n body, truncated, src = _capped_payload(\n call.get(\"prompt_text\"), \"dc.gateway_requests.json\")\n fence = _pick_fence(body)\n lines.append(\"**Prompt** (full input sent to the model):\")\n lines.append(fence)\n lines.append(body)\n if truncated:\n lines.append(f\"…[truncated; full prompt in {src}]\")\n lines.append(fence)\n lines.append(\"\")\n\n # Response block — only the full-tree path carries response_text;\n # gateway-direct rows get finish_reason in the header line above and\n # nothing else (the response DMO doesn't carry text on that path).\n if show_response_text:\n body, truncated, src = _capped_payload(\n call.get(\"response_text\"), \"dc.generations.json\")\n # html.unescape so the rendered block reads as plain JSON instead of\n # "-laden text — matches the existing _decoded_line treatment.\n if body and body != \"(empty)\":\n body = html.unescape(body)\n fence = _pick_fence(body)\n lines.append(\"**Response** (model output, including tool invocations):\")\n lines.append(fence)\n lines.append(body)\n if truncated:\n lines.append(f\"…[truncated; full response in {src}]\")\n lines.append(fence)\n lines.append(\"\")\n\n return lines\n\n\ndef _section_gateway_per_call_detail(sess: dict, *,\n show_prompts: bool = False) -> List[str]:\n \"\"\"Per-call detail for the gateway-direct branch.\n\n ``gateway_chain`` does NOT carry `response_text` (the responses DMO\n doesn't include it on that path), so the response block is always\n suppressed. The prompt block is gated by ``show_prompts`` so the\n default summary doesn't leak full prompt text — matches the\n documented contract for ``dc._session_summary.md``.\n \"\"\"\n chain = sess.get(\"gateway_chain\") or []\n lines: List[str] = [\"## Per-call detail\", \"\"]\n for i, call in enumerate(chain, start=1):\n lines.extend(_render_call_detail_block(\n call, i, show_prompts=show_prompts, show_response_text=False))\n return lines\n\n\ndef _collect_planner_llm_calls(sess: dict) -> List[dict]:\n \"\"\"Walk ``interactions[].steps[]`` and collect call-view dicts.\n\n Each step that has a ``gateway_request`` (regardless of binding method)\n contributes one call view. ``response_text`` is sourced from the step's\n sibling ``generation.response_text`` when present; otherwise the call is\n still emitted with the prompt + token summary.\n\n Returned dicts use the same field names as ``gateway_chain`` entries so\n ``_render_call_detail_block`` can consume both shapes uniformly.\n \"\"\"\n calls: List[dict] = []\n for iv in sess.get(\"interactions\") or []:\n for st in iv.get(\"steps\") or []:\n gw = st.get(\"gateway_request\")\n if not gw:\n continue\n gen = st.get(\"generation\") or {}\n calls.append({\n \"gateway_request_id\": gw.get(\"gateway_request_id\"),\n \"model\": gw.get(\"model\"),\n \"provider\": gw.get(\"provider\"),\n \"prompt_template_dev_name\": gw.get(\"prompt_template_dev_name\"),\n \"prompt_tokens\": gw.get(\"prompt_tokens\"),\n \"completion_tokens\": gw.get(\"completion_tokens\"),\n \"total_tokens\": gw.get(\"total_tokens\"),\n \"prompt_text\": gw.get(\"prompt_text\"),\n \"response\": gw.get(\"response\"),\n \"response_text\": gen.get(\"response_text\"),\n })\n return calls\n\n\ndef _section_planner_llm_calls(sess: dict, *, show_prompts: bool) -> List[str]:\n \"\"\"Opt-in full-tree section showing the input prompt + response\n for every LLM call in the session's hierarchical trace.\n\n Off by default — prompts can be 30 KB+ each on multi-turn sessions\n and would otherwise dominate the summary. Enable with\n ``render_dc.py --show-prompts``.\n \"\"\"\n if not show_prompts:\n return []\n calls = _collect_planner_llm_calls(sess)\n if not calls:\n return []\n lines: List[str] = [\n \"## Planner LLM calls (full prompts + responses)\",\n \"\",\n f\"_Found {len(calls)} LLM call(s) across the session's hierarchical \"\n f\"trace. Prompts are capped at \"\n f\"{_PROMPT_DISPLAY_CAP_BYTES // 1024} KB for display; full payloads \"\n f\"are on disk in `dc.gateway_requests.json` and \"\n f\"`dc.generations.json`._\",\n \"\",\n ]\n for i, call in enumerate(calls, start=1):\n # show_prompts is True here by section-guard above (early return when\n # show_prompts is False); pass through explicitly so the helper's new\n # prompt-gate stays aligned with the section's intent.\n lines.extend(_render_call_detail_block(\n call, i, show_prompts=True, show_response_text=True))\n return lines\n\n\ndef _assert_schema_version(tree: dict) -> None:\n \"\"\"Refuse unsupported versions; warn on missing version (older assembler).\"\"\"\n version = (tree.get(\"session\") or {}).get(\"_schema_version\")\n if version is None:\n print(\n \"render_dc: WARN tree has no _schema_version \"\n \"(produced by an older assembler?); rendering anyway\",\n file=sys.stderr,\n )\n return\n if version != _SUPPORTED_SCHEMA_VERSION:\n raise SystemExit(\n f\"render_dc: unsupported tree _schema_version={version}; \"\n f\"expected {_SUPPORTED_SCHEMA_VERSION}\"\n )\n\n\ndef main_for_session(sid: str, *, show_prompts: bool = False) -> int:\n \"\"\"Read session tree + manifest from the nested session dir; emit summary.md.\n\n Uses ``assemble_dc._find_session_dir`` to locate the session under\n ``DATA_ROOT/\u003corg>/\u003cagent>__\u003cver>/\u003csid>/`` — follows the ``_sessions/*.link``\n breadcrumb when present, globs otherwise. No callers need to know the\n full identity triple upfront.\n\n ``show_prompts``: pass through to ``render`` to include the\n opt-in \"Planner LLM calls\" section.\n \"\"\"\n from assemble_dc import _find_session_dir\n session_dir = _find_session_dir(sid)\n tree_path = session_dir / \"dc._session_tree.json\"\n if not tree_path.is_file():\n raise SystemExit(\n f\"render_dc: tree not found at {tree_path}; \"\n f\"run `python3 scripts/assemble_dc.py --session {sid}` first\"\n )\n manifest_path = session_dir / \"dc._session_manifest.json\"\n manifest: Optional[dict] = None\n if manifest_path.is_file():\n try:\n manifest = json.loads(manifest_path.read_text())\n except (json.JSONDecodeError, OSError) as e:\n print(\n f\"render_dc: WARN could not read manifest: {str(e).splitlines()[0]}\",\n file=sys.stderr,\n )\n\n tree = json.loads(tree_path.read_text())\n _assert_schema_version(tree)\n\n md_path = session_dir / \"dc._session_summary.md\"\n md_path.write_text(render(tree, manifest, session_dir=session_dir,\n show_prompts=show_prompts))\n print(f\"render_dc: wrote {md_path}\", file=sys.stderr)\n return 0\n\n\ndef main() -> int:\n ap = argparse.ArgumentParser(\n prog=\"render_dc.py\",\n description=\"Render dc._session_summary.md from dc._session_tree.json for one session.\",\n )\n ap.add_argument(\"--session\", required=True,\n help=\"AI-agent session UUID or MessagingSession id (0Mw...). \"\n \"Messaging ids are resolved from disk \"\n \"(DATA_ROOT/*/dc.sessions.json); run fetch_dc.py first \"\n \"if the session hasn't been fetched yet.\")\n ap.add_argument(\"--show-prompts\", action=\"store_true\",\n help=\"Include the opt-in 'Planner LLM calls' section \"\n \"with the full input prompt + response per LLM call. \"\n \"Off by default — multi-turn sessions can produce \"\n \"hundreds of KB here. Per-prompt display is capped \"\n \"at 64 KB; full payloads remain on disk in \"\n \"dc.gateway_requests.json + dc.generations.json.\")\n # Runtime-agnostic path overrides; default to ~/.vibe/...\n from _shared.cli_override import add_cli_flags, apply_overrides\n add_cli_flags(ap)\n args = ap.parse_args()\n apply_overrides(args, caller_globals=globals())\n from resolve_session import resolve_disk_or_live\n sid = resolve_disk_or_live(args.session)\n return main_for_session(sid, show_prompts=args.show_prompts)\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":71327,"content_sha256":"7710cf5fe81d4fd94a6cb7d12e20f1227b7f57d23033435727e5dc729f9a92c7"},{"filename":"scripts/resolve_session.py","content":"\"\"\"Resolve either an AI-agent session UUID or a Messaging session id (0Mw...)\nto the canonical AI-agent session UUID that the rest of the skill uses.\n\n## Why this exists\n\n`ssot__AIAgentSession__dlm.ssot__RelatedMessagingSessionId__c` stores the\nService Cloud `MessagingSession.Id` (18-char Salesforce id, prefix `0Mw`).\nUsers often only have that messaging id on hand, not the AI-agent session\nUUID (`019dface-0000-7000-8000-000000000002`). This resolver lets every\nentry point in the skill accept either form — it normalises to the UUID,\nand artifacts continue to land under `DATA_ROOT/\u003cuuid>/` as before.\n\n## Many-to-one is real\n\nLive-verified on an internal test org: 7 distinct messaging ids map to\nmultiple agent sessions; one id mapped to 5 separate agent sessions.\n\"Most recent by start_ts\" is NOT a safe default for disambiguation —\nthe user could legitimately want any of them. On multi-match we list\nevery candidate and exit non-zero; the user re-invokes with the\nspecific UUID.\n\n## Two resolution modes\n\n1. **Live** — query DC against `--org` (CLI: defaults to the sf CLI default\n target org). Used by `fetch_dc.py` at the\n top of the pipeline (before any artifacts exist on disk).\n2. **Disk-first** — scan `DATA_ROOT/*/dc.sessions.json` for a row whose\n `ssot__RelatedMessagingSessionId__c` matches. No DC call, no `--org`\n needed. Used by `assemble_dc.py` and `render_dc.py`. If the session\n has never been fetched, returns None and the caller errors with a\n pointer to `fetch_dc.py`.\n\n## CLI\n\n python3 scripts/resolve_session.py --id \u003cuuid|msg_id> [--org \u003calias>]\n\nPrints the resolved UUID to stdout on single-match (or pass-through).\nNon-zero exit with diagnostics on zero-match and multi-match.\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport sys\nfrom pathlib import Path\nfrom typing import List, Optional\n\nsys.path.insert(0, str(Path(__file__).parent))\n\nfrom config import DATA_ROOT\nfrom dc import default_target_org, load_sql, post, resolve_org\n\n\n# NOT_SET sentinel set. Matches fetch_dc.py:41; keep in sync.\n_NOT_SET = {\"\", \"NOT_SET\", None}\n\n\ndef is_messaging_id(s: str) -> bool:\n \"\"\"Shape check for a Salesforce MessagingSession id. The official key\n prefix is `0Mw`; the id is 15 or 18 chars. UUIDs are 36 chars with\n dashes, so they can't accidentally match.\"\"\"\n if not s:\n return False\n return s.startswith(\"0Mw\") and len(s) in (15, 18)\n\n\n# ---- live resolution (queries Data Cloud) --------------------------------\n\ndef _live_lookup(msg_id: str, org: str) -> List[dict]:\n \"\"\"Query ssot__AIAgentSession__dlm for rows where RelatedMessagingSessionId\n matches `msg_id`. Returns raw row dicts (caller decides how to handle\n N=0, N=1, N>1).\n\n SQL comes from `assets/dc/messaging_session.sql` — same templating\n convention as every other DMO query in this skill. The `MSG_ID`\n placeholder is interpolated as a literal string, but the caller\n guarantees (via is_messaging_id) that the value is a Salesforce\n MessagingSession id: exact 15/18 chars, `0Mw` key prefix. No quote,\n no wildcard, no semicolon can pass that shape check.\n \"\"\"\n if not is_messaging_id(msg_id):\n raise ValueError(\n f\"_live_lookup: msg_id {msg_id!r} failed is_messaging_id() shape check\"\n )\n instance_url, token = resolve_org(org)\n sql = load_sql(\"messaging_session\", MSG_ID=msg_id)\n return post(sql, instance_url, token, \"resolve_session\")\n\n\ndef _format_multi_match(msg_id: str, rows: List[dict]) -> str:\n \"\"\"Human-readable error for the N>1 case. Lists every candidate so\n the user can pick and re-invoke.\"\"\"\n lines = [\n f\"resolve_session: messaging id {msg_id!r} matches {len(rows)} \"\n f\"ssot__AIAgentSession__dlm rows — pick one and re-invoke with its UUID:\",\n \"\",\n ]\n for r in rows:\n uuid = r.get(\"ssot__Id__c\")\n start = r.get(\"ssot__StartTimestamp__c\") or \"—\"\n end = r.get(\"ssot__EndTimestamp__c\") or \"—\"\n end_type = r.get(\"ssot__AiAgentSessionEndType__c\") or \"—\"\n channel = r.get(\"ssot__AiAgentChannelType__c\") or \"—\"\n lines.append(\n f\" {uuid} start={start} end={end} end_type={end_type} channel={channel}\"\n )\n lines.append(\"\")\n lines.append(\n f\"Example: python3 scripts/fetch_dc.py --session {rows[0].get('ssot__Id__c')} --org \u003calias>\"\n )\n return \"\\n\".join(lines)\n\n\ndef resolve(sid_or_msg: str, *, org: str) -> str:\n \"\"\"Resolve an AI-agent session UUID from either form (live DC query).\n Exits the process on zero- and multi-match with a diagnostic message.\n Pass-through for UUID input.\"\"\"\n if not is_messaging_id(sid_or_msg):\n return sid_or_msg # already a UUID (or unknown form — let caller fail)\n rows = _live_lookup(sid_or_msg, org)\n if len(rows) == 0:\n raise SystemExit(\n f\"resolve_session: no ssot__AIAgentSession__dlm row with \"\n f\"RelatedMessagingSessionId={sid_or_msg!r} in org {org!r}\"\n )\n if len(rows) > 1:\n raise SystemExit(_format_multi_match(sid_or_msg, rows))\n return rows[0][\"ssot__Id__c\"]\n\n\n# ---- disk-first resolution (no DC call) ----------------------------------\n\ndef resolve_from_disk(sid_or_msg: str) -> Optional[str]:\n \"\"\"For scripts that don't have `--org` wired up. Scans every\n ``dc.sessions.json`` under ``DATA_ROOT`` for a row whose\n ``ssot__RelatedMessagingSessionId__c`` matches ``sid_or_msg``. Returns\n the UUID (from the matching row's ``ssot__Id__c``) or None if not found.\n\n Layout: the nested scheme is ``DATA_ROOT/\u003corg_id15>/\u003cagent>__\u003cver>/\u003cuuid>/\n dc.sessions.json``. Historic flat runs (``DATA_ROOT/\u003cuuid>/...``) are\n picked up by the same ``rglob`` walk, so this resolver works across\n both layouts without caller awareness.\n\n Archive suffix: user-created duplicate dirs named like\n ``\u003cuuid> - archive 1/`` carry stale copies of the same rows and would\n otherwise trigger spurious multi-match exits. They are skipped at\n scan time.\n\n Pass-through if input is already a UUID; returns input unchanged\n regardless of disk presence (caller validates with its own load-\n artifact check).\n \"\"\"\n if not is_messaging_id(sid_or_msg):\n return sid_or_msg # UUID — pass through (caller validates disk presence)\n\n matches: List[str] = [] # uuids\n if not DATA_ROOT.is_dir():\n return None\n # Nested layout is 4-deep (\u003corg>/\u003cagent>/\u003cuuid>/dc.sessions.json), flat\n # is 2-deep (\u003cuuid>/dc.sessions.json). rglob handles both without a\n # hard-coded depth.\n for sessions_p in sorted(DATA_ROOT.rglob(\"dc.sessions.json\")):\n if not sessions_p.is_file():\n continue\n # Skip `\u003cuuid> - archive N/` duplicate dirs — they hold stale copies\n # of the same rows and would produce spurious multi-match exits.\n if \" - archive\" in sessions_p.parent.name:\n continue\n try:\n data = json.loads(sessions_p.read_text())\n except (json.JSONDecodeError, OSError):\n continue\n # `dc.sessions.json` is a list of rows (the skill saves raw DC rows).\n if not isinstance(data, list):\n continue\n for row in data:\n row_msg = (row.get(\"ssot__RelatedMessagingSessionId__c\") or \"\").strip()\n if row_msg in _NOT_SET:\n continue\n if row_msg == sid_or_msg:\n uuid = row.get(\"ssot__Id__c\")\n if uuid:\n matches.append(uuid)\n if not matches:\n return None\n # De-duplicate across multiple dc.sessions.json files that happen to\n # contain the same row (possible if a user copies artifacts between\n # agent dirs). Distinct UUIDs remain a multi-match.\n unique = sorted(set(matches))\n if len(unique) > 1:\n raise SystemExit(\n f\"resolve_session: messaging id {sid_or_msg!r} matches {len(unique)} \"\n f\"session directories on disk — pick a UUID and re-invoke:\\n \"\n + \"\\n \".join(unique)\n )\n return unique[0]\n\n\ndef resolve_disk_or_live(sid_or_msg: str, org: Optional[str] = None) -> str:\n \"\"\"Combined path used by entry points that support both. Tries disk\n first (cheap); falls back to live DC only if `org` is supplied.\n Raises with a useful message if disk-miss and no org.\"\"\"\n if not is_messaging_id(sid_or_msg):\n return sid_or_msg # UUID — no resolution needed\n disk = resolve_from_disk(sid_or_msg)\n if disk is not None:\n return disk\n if org is None:\n raise SystemExit(\n f\"resolve_session: cannot resolve messaging id {sid_or_msg!r} from \"\n f\"disk (no dc.sessions.json under DATA_ROOT has a matching \"\n f\"RelatedMessagingSessionId). Run `python3 scripts/fetch_dc.py \"\n f\"--session {sid_or_msg} --org \u003calias>` first, or re-invoke with \"\n f\"the AI-agent session UUID directly.\"\n )\n return resolve(sid_or_msg, org=org)\n\n\n# ---- CLI -----------------------------------------------------------------\n\ndef main() -> int:\n ap = argparse.ArgumentParser(\n description=\"Resolve a Salesforce messaging session id (0Mw...) to the \"\n \"AI-agent session UUID the skill uses. Passes UUID input through unchanged.\"\n )\n ap.add_argument(\"--id\", required=True,\n help=\"Either the AI-agent session UUID or the MessagingSession id (0Mw...)\")\n ap.add_argument(\"--org\", help=\"sf org alias for the live DC lookup when input is 0Mw... \"\n \"(default: the sf CLI default target org)\")\n ap.add_argument(\"--disk-only\", action=\"store_true\",\n help=\"Only scan DATA_ROOT/*/dc.sessions.json; no DC query\")\n # Runtime-agnostic path overrides; default to ~/.vibe/...\n from _shared.cli_override import add_cli_flags, apply_overrides\n add_cli_flags(ap)\n args = ap.parse_args()\n apply_overrides(args, caller_globals=globals())\n\n if not is_messaging_id(args.id):\n print(args.id)\n return 0\n if args.disk_only:\n found = resolve_from_disk(args.id)\n if found is None:\n print(f\"resolve_session: no local session dir matches {args.id!r}\", file=sys.stderr)\n return 1\n print(found)\n return 0\n org = args.org\n if not org:\n # Try disk first as a convenience; on a miss, fall back to the sf CLI\n # default target org for the live lookup, and only error if that\n # is unset too.\n disk = resolve_from_disk(args.id)\n if disk is not None:\n print(disk)\n return 0\n try:\n org = default_target_org()\n except SystemExit as e:\n print(\n f\"resolve_session: {args.id!r} not found on disk; --org \u003calias> required \"\n f\"for a live DC lookup ({e})\",\n file=sys.stderr,\n )\n return 2\n uuid = resolve(args.id, org=org)\n print(uuid)\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":11105,"content_sha256":"7947431c1233a1b28a0b03bc5820f1aa4bd64eba08fdfc4de483316d7e67d694"},{"filename":"scripts/storage.py","content":"\"\"\"Shared per-session JSON writer.\n\nEvery Data Cloud artifact lands under the nested layout:\n\n DATA_ROOT/\u003corg_id_15>/\u003cagent_api_name>__\u003cagent_version>/\u003csession_id>/\u003csource>.\u003cname>.json\n\nOne save, one shape, one directory convention — imported by every caller.\n\n**Security contract:**\nAll four path segments (``org_id_15``, ``agent_api_name``, ``agent_version``,\n``session_id``) flow through ``paths.session_dir(...)``, which validates\neach via fs_guard (regex) before the join. A ``..`` or ``/`` in any\nsegment raises ``paths.PathValidationError`` — direct\n``DATA_ROOT / session_id`` composition is unreachable from this module.\n\nSource-prefix convention:\n Flat directory, prefix filenames with provenance so an `ls` on the\n per-session dir tells you where each artifact came from:\n\n dc.sessions.json — from Data Cloud\n dc.interactions.json\n\n Baking the prefix into the API (vs. leaving it to filename discipline)\n makes it impossible to forget — callers pass ``source`` + ``name``,\n never a raw filename.\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nfrom pathlib import Path\n\nfrom config import paths\n\n\ndef save(\n data: list[dict] | dict,\n org_id_15: str,\n agent_api_name: str,\n agent_version: str,\n session_id: str,\n source: str,\n name: str,\n) -> Path:\n \"\"\"Write JSON under the nested session dir. Returns the target path.\n\n Signature:\n save(data, org_id_15, agent_api_name, agent_version, session_id,\n source, name) -> Path\n\n ``source`` is the leading filename segment for provenance — callers\n pass ``\"dc\"``. ``name`` is the bare artifact name, no extension —\n ``.json`` is appended.\n\n Raises ``paths.PathValidationError`` if any of the four identity\n segments fails validation.\n \"\"\"\n # paths.session_dir() validates org_id_15, agent_api_name, agent_version\n # (via fs_guard) and session_id (via SESSION_ID_RE.fullmatch). Any bad\n # input raises PathValidationError before we touch the filesystem.\n target = paths.session_dir(\n org_id_15, agent_api_name, agent_version, session_id\n )\n target.mkdir(parents=True, exist_ok=True)\n path = target / f\"{source}.{name}.json\"\n path.write_text(json.dumps(data, indent=2, default=str) + \"\\n\")\n _write_breadcrumb(org_id_15, agent_api_name, agent_version, session_id)\n return path\n\n\ndef _write_breadcrumb(\n org_id_15: str,\n agent_api_name: str,\n agent_version: str,\n session_id: str,\n) -> None:\n \"\"\"Write ``\u003corg>/_sessions/\u003csid>.link`` pointing at the session dir.\n\n Plain text, no symlink (Windows-safe). Content is the relative path\n ``../\u003cagent>__\u003cver>/\u003csid>\\\\n``. Idempotent — overwriting with the\n same content is a no-op semantically. Silent on failure; breadcrumbs\n are best-effort (a missing breadcrumb doesn't break the write,\n only handoff-session discovery).\n \"\"\"\n try:\n link_dir = paths.DATA_ROOT / org_id_15 / \"_sessions\"\n link_dir.mkdir(parents=True, exist_ok=True)\n link_path = link_dir / f\"{session_id}.link\"\n link_path.write_text(\n f\"../{agent_api_name}__{agent_version}/{session_id}\\n\"\n )\n except OSError:\n pass\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3224,"content_sha256":"fd7c43b16f1cf17c93a5df40c6b4df408ea559471c9b37d152529742551a5964"},{"filename":"scripts/tests/__init__.py","content":"","content_type":"text/x-python; charset=utf-8","language":"python","size":0,"content_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"filename":"scripts/tests/_bootstrap.py","content":"\"\"\"Test bootstrap — adds sibling ``scripts/`` to sys.path.\n\nEvery ``test_*.py`` in this directory imports this module first\n(as ``from . import _bootstrap``) so the sibling modules\n(`config`, `storage`, `paths`, etc.) resolve.\n\"\"\"\nfrom __future__ import annotations\n\nimport sys\nfrom pathlib import Path\n\n# scripts/tests/_bootstrap.py → scripts/\n_SCRIPTS_DIR = Path(__file__).resolve().parent.parent\nif str(_SCRIPTS_DIR) not in sys.path:\n sys.path.insert(0, str(_SCRIPTS_DIR))\n","content_type":"text/x-python; charset=utf-8","language":"python","size":483,"content_sha256":"b7e5fac3eb8fc8571977af4e7d1b5ab975a1549795240ab119aec0596104bb5c"},{"filename":"scripts/tests/fixtures/__init__.py","content":"","content_type":"text/x-python; charset=utf-8","language":"python","size":0,"content_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"filename":"scripts/tests/fixtures/synthetic_session.py","content":"\"\"\"Synthetic session fixture for d360 integration tests.\n\nProvides:\n\n- ``IDS`` a flat namespace of all UUIDs/keys used so tests\n can assert against specific values.\n- ``make_rows()`` returns ``{dmo_name: [row, ...]}`` for the 24 DMOs\n `assemble_dc` reads. Self-consistent: every\n Step.GenerationId references a Generation row, etc.\n- ``make_manifest`` returns the dc._session_manifest.json shape.\n- ``write_to_disk`` materializes the whole fixture under a session dir\n so tests that drive the public ``assemble_dc.assemble``\n can use ``tmp_path`` + DATA_ROOT redirect without\n duplicating layout knowledge.\n\nShape rules:\n - Timestamps anchored on 2026-04-22T10:00:00Z; offsets in seconds.\n - All ssot__Id__c values are 18-char-stable strings (not real Salesforce\n IDs) — assemble_dc only key-equality-checks them.\n - HTML escaping applied to AttributeText__c so the trace_id fallback\n path exercises real code.\n - parameters__c on gateway_responses uses the "-escaped JSON shape\n real DC emits.\n\"\"\"\nfrom __future__ import annotations\n\nimport html\nimport json\nfrom copy import deepcopy\nfrom pathlib import Path\nfrom typing import Any, Dict, List\n\n\n# -----------------------------------------------------------------------------\n# IDs — exposed flat for cross-test assertions\n# -----------------------------------------------------------------------------\n\n\nclass _IDs:\n SID = \"019dface-0000-7000-8000-000000000001\"\n \"\"\"Agent session UUID. Valid UUIDv7 so window decode works.\"\"\"\n\n ORG_ID_15 = \"00D000000000ABC\"\n ORG_ID_18 = \"00D000000000ABCDEF\"\n AGENT_API = \"DemoAgent\"\n AGENT_VERSION = \"v5\"\n BOT_ID = \"0Xx000000000DemoBot\"\n\n # Interactions\n IXN_START = \"ixn-start-001\"\n IXN_TURN = \"ixn-turn-002\"\n IXN_END = \"ixn-end-003\"\n\n # Steps within the TURN\n STEP_TOPIC = \"step-topic-001\"\n STEP_ACTION = \"step-action-002\"\n STEP_GUARDRAIL = \"step-guardrail-003\"\n\n # Gateway audit chain — one declared, one timestamp-window-only\n GW_REQ_DECLARED = \"gw-req-declared-001\"\n GW_REQ_WINDOW = \"gw-req-window-002\"\n GW_RESP_DECLARED = \"gw-resp-declared-001\"\n\n # Generation chain (declared)\n GEN_DECLARED = \"gen-declared-001\"\n\n # Participants\n PART_AGENT = \"part-agent-001\"\n PART_USER = \"part-user-002\"\n\n # Trace + window\n TRACE_ID = \"8b820e85aaaaaaaabbbbbbbbcccccccc\"\n SESSION_START = \"2026-04-22T10:00:00.000Z\"\n SESSION_END = \"2026-04-22T10:01:30.000Z\"\n TURN_START = \"2026-04-22T10:00:05.000Z\"\n TURN_END = \"2026-04-22T10:01:00.000Z\"\n GW_DECLARED_TS = \"2026-04-22T10:00:30.000Z\" # inside step-action window\n GW_WINDOW_TS = \"2026-04-22T10:00:45.000Z\" # inside the TURN window only\n\n\nIDS = _IDs\n\n\n# -----------------------------------------------------------------------------\n# Per-DMO row builders\n# -----------------------------------------------------------------------------\n\n\ndef _sessions() -> List[dict]:\n return [{\n \"ssot__Id__c\": IDS.SID,\n \"ssot__InternalOrganizationId__c\": IDS.ORG_ID_18,\n \"ssot__StartTimestamp__c\": IDS.SESSION_START,\n \"ssot__EndTimestamp__c\": IDS.SESSION_END,\n \"ssot__AiAgentChannelType__c\": \"SCRT2 - EmbeddedMessaging\",\n \"ssot__AiAgentSessionEndType__c\": \"USER_ENDED\",\n \"ssot__RelatedMessagingSessionId__c\": \"0MwTESTMSG12345AAA\",\n }]\n\n\ndef _interactions() -> List[dict]:\n \"\"\"Three interactions: SESSION_START, TURN, SESSION_END.\n\n `AttributeText__c` carries the real internalTraceId in HTML-escaped\n JSON form on the TURN row (matches live DC shape).\n \"\"\"\n # No spaces in separators — production logger emits compact JSON, and\n # the trace_id regex (`\"internalTraceId\":\"([a-f0-9]+)\"`) requires no\n # whitespace around the colon.\n raw_attr_text = json.dumps(\n {\"internalTraceId\": IDS.TRACE_ID, \"label\": \"react-loop\"},\n separators=(\",\", \":\"),\n )\n return [\n {\n \"ssot__Id__c\": IDS.IXN_START,\n \"ssot__AiAgentSessionId__c\": IDS.SID,\n \"ssot__AiAgentInteractionType__c\": \"SESSION_START\",\n \"ssot__StartTimestamp__c\": IDS.SESSION_START,\n \"ssot__EndTimestamp__c\": IDS.SESSION_START,\n \"ssot__TelemetryTraceId__c\": \"\",\n \"ssot__AttributeText__c\": \"\",\n },\n {\n \"ssot__Id__c\": IDS.IXN_TURN,\n \"ssot__AiAgentSessionId__c\": IDS.SID,\n \"ssot__AiAgentInteractionType__c\": \"TURN\",\n \"ssot__StartTimestamp__c\": IDS.TURN_START,\n \"ssot__EndTimestamp__c\": IDS.TURN_END,\n \"ssot__TelemetryTraceId__c\": \"\",\n # HTML-escaped JSON triggers the AttributeText fallback path.\n \"ssot__AttributeText__c\": html.escape(raw_attr_text, quote=True),\n },\n {\n \"ssot__Id__c\": IDS.IXN_END,\n \"ssot__AiAgentSessionId__c\": IDS.SID,\n \"ssot__AiAgentInteractionType__c\": \"SESSION_END\",\n \"ssot__StartTimestamp__c\": IDS.SESSION_END,\n \"ssot__EndTimestamp__c\": IDS.SESSION_END,\n \"ssot__TelemetryTraceId__c\": \"NOT_SET\",\n \"ssot__AttributeText__c\": \"\",\n },\n ]\n\n\ndef _steps() -> List[dict]:\n \"\"\"Three steps inside the TURN. Step times nest inside the turn window\n so the timestamp-window pass binds gateway_requests by containment.\"\"\"\n return [\n {\n \"ssot__Id__c\": IDS.STEP_TOPIC,\n \"ssot__AiAgentInteractionId__c\": IDS.IXN_TURN,\n \"ssot__AiAgentInteractionStepType__c\": \"TOPIC_STEP\",\n \"ssot__StartTimestamp__c\": \"2026-04-22T10:00:10.000Z\",\n \"ssot__EndTimestamp__c\": \"2026-04-22T10:00:20.000Z\",\n \"ssot__GenerationId__c\": \"NOT_SET\",\n \"ssot__Name__c\": \"TopicSelection\",\n },\n {\n \"ssot__Id__c\": IDS.STEP_ACTION,\n \"ssot__AiAgentInteractionId__c\": IDS.IXN_TURN,\n \"ssot__AiAgentInteractionStepType__c\": \"ACTION_STEP\",\n \"ssot__StartTimestamp__c\": \"2026-04-22T10:00:25.000Z\",\n \"ssot__EndTimestamp__c\": \"2026-04-22T10:00:40.000Z\",\n \"ssot__GenerationId__c\": IDS.GEN_DECLARED,\n \"ssot__Name__c\": \"PrimaryAction\",\n },\n {\n \"ssot__Id__c\": IDS.STEP_GUARDRAIL,\n \"ssot__AiAgentInteractionId__c\": IDS.IXN_TURN,\n \"ssot__AiAgentInteractionStepType__c\": \"TRUST_GUARDRAILS_STEP\",\n \"ssot__StartTimestamp__c\": \"2026-04-22T10:00:55.000Z\",\n \"ssot__EndTimestamp__c\": \"2026-04-22T10:01:00.000Z\",\n \"ssot__GenerationId__c\": \"NOT_SET\",\n \"ssot__Name__c\": \"FinalGuard\",\n },\n ]\n\n\ndef _messages() -> List[dict]:\n return [\n {\n \"ssot__Id__c\": \"msg-001\",\n \"ssot__AiAgentInteractionId__c\": IDS.IXN_TURN,\n \"ssot__AiAgentInteractionMessageType__c\": \"USER\",\n \"ssot__ContentText__c\": \"How do I refund my order?\",\n \"ssot__StartTimestamp__c\": IDS.TURN_START,\n },\n {\n \"ssot__Id__c\": \"msg-002\",\n \"ssot__AiAgentInteractionId__c\": IDS.IXN_TURN,\n \"ssot__AiAgentInteractionMessageType__c\": \"AGENT\",\n \"ssot__ContentText__c\": \"I'll help you with the refund process.\",\n \"ssot__StartTimestamp__c\": IDS.TURN_END,\n },\n ]\n\n\ndef _participants() -> List[dict]:\n return [\n {\n \"ssot__Id__c\": IDS.PART_AGENT,\n \"ssot__AiAgentSessionId__c\": IDS.SID,\n \"ssot__AiAgentSessionParticipantRole__c\": \"AGENT\",\n \"ssot__AiAgentApiName__c\": IDS.AGENT_API,\n \"ssot__AiAgentVersionApiName__c\": IDS.AGENT_VERSION,\n },\n {\n \"ssot__Id__c\": IDS.PART_USER,\n \"ssot__AiAgentSessionId__c\": IDS.SID,\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"\",\n \"ssot__AiAgentVersionApiName__c\": \"\",\n },\n ]\n\n\ndef _gateway_requests() -> List[dict]:\n \"\"\"Two gateway requests:\n\n - GW_REQ_DECLARED: chained from Step → Generation → Response → Request.\n Timestamp also falls within step-action window so either binding\n method can resolve it. Declared chain wins.\n\n - GW_REQ_WINDOW: NOT in the declared chain. Timestamp falls inside\n the TURN window → exercise timestamp-window fallback.\n \"\"\"\n return [\n {\n \"gatewayRequestId__c\": IDS.GW_REQ_DECLARED,\n \"sf__Id\": \"sf-\" + IDS.GW_REQ_DECLARED,\n \"sessionId__c\": f'\"{IDS.SID}\"',\n \"timestamp__c\": IDS.GW_DECLARED_TS,\n \"model__c\": \"gpt-4o\",\n \"provider__c\": \"openai\",\n \"promptTemplateDevName__c\": \"AiCopilot__ReactInitialPrompt\",\n \"feature__c\": \"plannerservice\",\n \"promptTokens__c\": 1200,\n \"completionTokens__c\": 50,\n \"totalTokens__c\": 1250,\n \"prompt__c\": \"Declared-bound prompt body.\",\n },\n {\n \"gatewayRequestId__c\": IDS.GW_REQ_WINDOW,\n \"sf__Id\": \"sf-\" + IDS.GW_REQ_WINDOW,\n \"sessionId__c\": f'\"{IDS.SID}\"',\n \"timestamp__c\": IDS.GW_WINDOW_TS,\n \"model__c\": \"gpt-4o-mini\",\n \"provider__c\": \"openai\",\n \"promptTemplateDevName__c\": \"AiCopilot__PromptTemplateGenerationsInvocable\",\n \"feature__c\": \"plannerservice\",\n \"promptTokens__c\": 200,\n \"completionTokens__c\": 20,\n \"totalTokens__c\": 220,\n \"prompt__c\": \"Window-bound prompt body.\",\n },\n ]\n\n\ndef _gateway_responses() -> List[dict]:\n raw = '{\"finish_reason\":\"\\\\\"stop\\\\\"\"}'\n escaped = raw.replace('\"', \""\")\n return [{\n \"generationResponseId__c\": IDS.GW_RESP_DECLARED,\n \"generationRequestId__c\": IDS.GW_REQ_DECLARED,\n \"parameters__c\": escaped,\n \"timestamp__c\": IDS.GW_DECLARED_TS,\n }]\n\n\ndef _generations() -> List[dict]:\n return [{\n \"generationId__c\": IDS.GEN_DECLARED,\n \"generationResponseId__c\": IDS.GW_RESP_DECLARED,\n \"responseText__c\": \"I'll help you with the refund process.\",\n \"timestamp__c\": IDS.GW_DECLARED_TS,\n }]\n\n\ndef _content_quality() -> List[dict]:\n return [{\n \"generationId__c\": IDS.GEN_DECLARED,\n \"trustScore__c\": 0.92,\n \"qualityType__c\": \"NORMAL\",\n }]\n\n\ndef _gateway_records() -> List[dict]:\n return [{\n \"id__c\": \"gw-rec-001\",\n \"parent__c\": IDS.GW_REQ_DECLARED,\n \"sessionId__c\": IDS.SID,\n \"recordType__c\": \"RAG\",\n }]\n\n\ndef _gateway_request_tags() -> List[dict]:\n return [\n {\"id__c\": \"tag-001\", \"parent__c\": IDS.GW_REQ_DECLARED,\n \"tag__c\": \"promptTemplateDevName\", \"tagValue__c\": \"AiCopilot__ReactInitialPrompt\"},\n {\"id__c\": \"tag-002\", \"parent__c\": IDS.GW_REQ_DECLARED,\n \"tag__c\": \"feature\", \"tagValue__c\": \"plannerservice\"},\n ]\n\n\n# -----------------------------------------------------------------------------\n# Public: rows + manifest\n# -----------------------------------------------------------------------------\n\n\n_ALL_TEMPLATES = (\n \"sessions\", \"interactions\", \"messages\", \"moments\", \"participants\",\n \"tag_associations\", \"gateway_requests\",\n \"steps\", \"moment_interactions\",\n \"telemetry_spans\", \"generations\", \"app_generation\",\n \"gateway_request_tags\", \"gateway_responses\", \"gateway_records\",\n \"gateway_request_metadata\", \"gateway_request_llm\",\n \"feedback\",\n \"content_quality\", \"content_category\", \"feedback_details\",\n \"tag_definitions\", \"tag_definition_associations\", \"tags\",\n)\n\n\ndef make_rows() -> Dict[str, List[dict]]:\n \"\"\"Return ``{dmo_name: [row, ...]}`` for all 24 DMOs.\n\n Empty lists for DMOs the synthetic session doesn't exercise — the\n code under test reads them by ``.get(name, [])`` and tolerates\n missing entries.\n \"\"\"\n populated = {\n \"sessions\": _sessions(),\n \"interactions\": _interactions(),\n \"steps\": _steps(),\n \"messages\": _messages(),\n \"participants\": _participants(),\n \"gateway_requests\": _gateway_requests(),\n \"gateway_responses\": _gateway_responses(),\n \"generations\": _generations(),\n \"content_quality\": _content_quality(),\n \"gateway_records\": _gateway_records(),\n \"gateway_request_tags\": _gateway_request_tags(),\n }\n rows: Dict[str, List[dict]] = {name: [] for name in _ALL_TEMPLATES}\n rows.update(populated)\n return deepcopy(rows)\n\n\ndef make_manifest() -> dict:\n \"\"\"Manifest shape matching what `fetch_dc.py` writes.\"\"\"\n rows = make_rows()\n queries = [\n {\n \"name\": name,\n \"row_count\": len(rows[name]),\n \"elapsed_ms\": 50,\n \"where_clause\": f\"ssot__AiAgentSessionId__c = '{IDS.SID}'\",\n }\n for name in _ALL_TEMPLATES\n ]\n return {\n \"_doc\": \"Per-query summary of this DC fetch run.\",\n \"session_id\": IDS.SID,\n \"org_alias\": \"my-org\",\n \"instance_url\": \"https://example.my.salesforce.com\",\n \"org_id_15\": IDS.ORG_ID_15,\n \"agent_api_name\": IDS.AGENT_API,\n \"agent_version\": IDS.AGENT_VERSION,\n \"session_shape\": \"ok\",\n \"started_at_utc\": \"2026-04-22T10:01:30.000Z\",\n \"finished_at_utc\": \"2026-04-22T10:01:32.000Z\",\n \"elapsed_ms\": 2000,\n \"queries\": queries,\n \"harvested_ids\": {\n \"interactions\": [IDS.IXN_START, IDS.IXN_TURN, IDS.IXN_END],\n },\n }\n\n\n# -----------------------------------------------------------------------------\n# Disk materialization\n# -----------------------------------------------------------------------------\n\n\ndef session_dir_for(data_root: Path) -> Path:\n \"\"\"Compute the canonical session dir under a tmp DATA_ROOT.\"\"\"\n return (\n data_root\n / IDS.ORG_ID_15\n / f\"{IDS.AGENT_API}__{IDS.AGENT_VERSION}\"\n / IDS.SID\n )\n\n\ndef write_to_disk(data_root: Path) -> Path:\n \"\"\"Materialize the synthetic session under ``\u003cdata_root>/\u003corg>/\u003cagent>__\u003cver>/\u003csid>/``.\n\n Writes:\n - dc._session_manifest.json\n - dc.\u003cname>.json for every populated DMO\n - The per-org breadcrumb at \u003corg>/_sessions/\u003csid>.link\n\n Returns the session dir absolute path.\n \"\"\"\n sdir = session_dir_for(data_root)\n sdir.mkdir(parents=True, exist_ok=True)\n\n # DC manifest + rows\n (sdir / \"dc._session_manifest.json\").write_text(\n json.dumps(make_manifest(), indent=2) + \"\\n\"\n )\n rows = make_rows()\n for name, rs in rows.items():\n if rs:\n (sdir / f\"dc.{name}.json\").write_text(json.dumps(rs, indent=2) + \"\\n\")\n\n # Breadcrumb so _find_session_dir's primary path resolves\n link_dir = data_root / IDS.ORG_ID_15 / \"_sessions\"\n link_dir.mkdir(parents=True, exist_ok=True)\n (link_dir / f\"{IDS.SID}.link\").write_text(\n f\"../{IDS.AGENT_API}__{IDS.AGENT_VERSION}/{IDS.SID}\\n\"\n )\n\n return sdir\n","content_type":"text/x-python; charset=utf-8","language":"python","size":14921,"content_sha256":"da582bc288a6c620b348f70efa52d252301cf729e0e3776b00000f00d90e5975"},{"filename":"scripts/tests/test_assemble_dc_bootstrap_and_mode.py","content":"\"\"\"Tests for the bootstrap-variable parser + channel-mode deriver in assemble_dc.py.\n\nCoverage:\n - `_parse_bootstrap_variables`\n - returns None for missing/NOT_SET/UNSET_VALUE\n - parses well-formed JSON object\n - parses HTML-entity-encoded JSON\n - returns _parse_error dict for malformed JSON\n - returns _parse_error dict for non-object JSON (array, scalar)\n\n - `_derive_mode`\n - voice → when RelatedVoiceCallId set\n - production_messaging → when RelatedMessagingSessionId set\n - builder_previewer → when bootstrap has indicator keys\n - unknown → when nothing matches\n\"\"\"\nfrom __future__ import annotations\n\nimport unittest\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nfrom assemble_dc import _parse_bootstrap_variables, _derive_mode # type: ignore\n\n\nclass ParseBootstrapVariablesTests(unittest.TestCase):\n\n def test_none_input_returns_none(self):\n self.assertIsNone(_parse_bootstrap_variables(None))\n\n def test_empty_string_returns_none(self):\n self.assertIsNone(_parse_bootstrap_variables(\"\"))\n\n def test_not_set_sentinel_returns_none(self):\n self.assertIsNone(_parse_bootstrap_variables(\"NOT_SET\"))\n\n def test_unset_value_sentinel_returns_none(self):\n self.assertIsNone(_parse_bootstrap_variables(\"UNSET_VALUE\"))\n\n def test_well_formed_json_object_returns_dict(self):\n out = _parse_bootstrap_variables('{\"__resolved_locale__\":\"en_US\",\"x\":1}')\n self.assertEqual(out, {\"__resolved_locale__\": \"en_US\", \"x\": 1})\n\n def test_html_entity_encoded_json_unescaped_then_parsed(self):\n \"\"\"Some surfaces emit `"`-encoded JSON. The parser unescapes\n first, then json.loads.\"\"\"\n encoded = \"{"__resolved_locale__":"en_US"}\"\n out = _parse_bootstrap_variables(encoded)\n self.assertEqual(out, {\"__resolved_locale__\": \"en_US\"})\n\n def test_malformed_json_returns_parse_error_dict(self):\n out = _parse_bootstrap_variables(\"{not-valid-json\")\n self.assertIsNotNone(out)\n self.assertTrue(out.get(\"_parse_error\"))\n self.assertIn(\"_raw\", out)\n\n def test_json_array_returns_parse_error_dict(self):\n \"\"\"VariableText__c is documented as an object; arrays/scalars are\n defensive cases that surface as _parse_error rather than crash.\"\"\"\n out = _parse_bootstrap_variables('[1, 2, 3]')\n self.assertTrue(out.get(\"_parse_error\"))\n\n def test_json_scalar_returns_parse_error_dict(self):\n out = _parse_bootstrap_variables('\"just a string\"')\n self.assertTrue(out.get(\"_parse_error\"))\n\n\nclass DeriveModeTests(unittest.TestCase):\n\n def test_voice_call_id_set_returns_voice(self):\n out = _derive_mode(\n messaging_session_id=\"msg-1\", # ignored when voice present\n voice_call_id=\"voice-1\",\n bootstrap_variables=None,\n )\n self.assertEqual(out, \"voice\")\n\n def test_messaging_session_id_set_returns_production_messaging(self):\n out = _derive_mode(\n messaging_session_id=\"0MwTESTMSG00000\",\n voice_call_id=None,\n bootstrap_variables=None,\n )\n self.assertEqual(out, \"production_messaging\")\n\n def test_builder_previewer_keys_in_bootstrap_returns_builder_previewer(self):\n bootstrap = {\"__resolved_locale__\": \"en_US\", \"OpenAgent\": \"x\"}\n out = _derive_mode(\n messaging_session_id=None,\n voice_call_id=None,\n bootstrap_variables=bootstrap,\n )\n self.assertEqual(out, \"builder_previewer\")\n\n def test_no_signals_returns_unknown(self):\n out = _derive_mode(\n messaging_session_id=None,\n voice_call_id=None,\n bootstrap_variables={\"OpenAgent\": \"x\"}, # no indicator keys\n )\n self.assertEqual(out, \"unknown\")\n\n def test_voice_takes_priority_over_messaging(self):\n \"\"\"Defensive: if both messaging_session_id and voice_call_id are\n set, voice wins (per priority order documented in the function).\"\"\"\n out = _derive_mode(\n messaging_session_id=\"msg-1\",\n voice_call_id=\"voice-1\",\n bootstrap_variables=None,\n )\n self.assertEqual(out, \"voice\")\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4306,"content_sha256":"087c6a408912fb251bd2fb303b902217eedde3d3b98ef2a5c2ae84347266f918"},{"filename":"scripts/tests/test_assemble_dc_gateway_direct_integration.py","content":"\"\"\"Integration tests for ``assemble_dc._assemble_gateway_direct``.\n\nDrives the materialization-lag path: session row + gateway_requests\npopulated, but interactions / steps absent. Triggered by setting\n``manifest.session_shape = \"interactions_not_materialized_yet\"``.\n\nComplements ``test_assemble_dc_gateway_direct.py`` which calls the\nprivate function with a hand-built rows dict; this file drives the\npublic ``assemble`` entry point through the disk-load path so manifest\n+ rows + path resolution are all exercised.\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport unittest\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport assemble_dc # type: ignore\nfrom config import paths # type: ignore\nfrom .fixtures.synthetic_session import ( # type: ignore\n IDS, write_to_disk, make_manifest,\n)\n\n\ndef _materialization_lag_mutator(sdir: Path) -> None:\n \"\"\"Make the fixture look like a session whose STDM hierarchy hasn't\n materialized yet: keep gateway_requests + gateway_responses + tags,\n but wipe interactions / steps / messages and flip the manifest.\n\n This is the live-DC failure mode that the gateway-direct path was\n written to handle.\n \"\"\"\n # Wipe interaction/step/message rows (file with empty list)\n for name in (\"interactions\", \"steps\", \"messages\"):\n (sdir / f\"dc.{name}.json\").write_text(\"[]\")\n # Update manifest session_shape so assemble's branch fires.\n manifest = make_manifest()\n manifest[\"session_shape\"] = \"interactions_not_materialized_yet\"\n (sdir / \"dc._session_manifest.json\").write_text(\n json.dumps(manifest, indent=2)\n )\n\n\nclass _GatewayDirectHarness:\n def __enter__(self):\n self._tmp = TemporaryDirectory()\n self._tmpdir = Path(self._tmp.name)\n sdir = write_to_disk(self._tmpdir)\n _materialization_lag_mutator(sdir)\n self._patch = mock.patch.object(paths, \"DATA_ROOT\", self._tmpdir)\n self._patch.start()\n self.tree, self.sdir = assemble_dc.assemble(IDS.SID)\n return self\n\n def __exit__(self, *exc):\n self._patch.stop()\n self._tmp.cleanup()\n\n\n# -----------------------------------------------------------------------------\n# Top-level shape on the gateway-direct branch\n# -----------------------------------------------------------------------------\n\n\nclass GatewayDirectShapeTests(unittest.TestCase):\n\n def test_source_marker_set_to_gateway_direct(self):\n with _GatewayDirectHarness() as h:\n self.assertEqual(h.tree[\"_source\"], \"gateway_direct\")\n\n def test_interactions_is_empty_list(self):\n with _GatewayDirectHarness() as h:\n self.assertEqual(h.tree[\"session\"][\"interactions\"], [])\n\n def test_gateway_chain_populated_from_gateway_requests(self):\n with _GatewayDirectHarness() as h:\n chain = h.tree[\"session\"][\"gateway_chain\"]\n # Fixture has 2 gateway_requests; both make it into the chain.\n self.assertEqual(len(chain), 2)\n chain_ids = {gw[\"gateway_request_id\"] for gw in chain}\n self.assertEqual(chain_ids, {IDS.GW_REQ_DECLARED, IDS.GW_REQ_WINDOW})\n\n def test_chain_sorted_by_timestamp(self):\n # Earlier gateway request timestamp comes first.\n with _GatewayDirectHarness() as h:\n chain = h.tree[\"session\"][\"gateway_chain\"]\n timestamps = [gw.get(\"timestamp\") for gw in chain]\n self.assertEqual(timestamps, sorted(t or \"\" for t in timestamps))\n\n def test_each_gateway_chain_entry_has_response(self):\n # Only the declared request has a response in the fixture; the\n # window-bound one has response=None. Both shapes are accepted.\n with _GatewayDirectHarness() as h:\n chain = h.tree[\"session\"][\"gateway_chain\"]\n declared = next(gw for gw in chain\n if gw[\"gateway_request_id\"] == IDS.GW_REQ_DECLARED)\n self.assertIsNotNone(declared[\"response\"])\n\n def test_top_identity_block_resolved_from_manifest(self):\n with _GatewayDirectHarness() as h:\n ident = h.tree[\"identity\"]\n self.assertEqual(ident[\"org_id_15\"], IDS.ORG_ID_15)\n self.assertEqual(ident[\"agent_api_name\"], IDS.AGENT_API)\n self.assertEqual(ident[\"agent_version\"], IDS.AGENT_VERSION)\n\n def test_counts_block_records_session_shape_and_zero_interactions(self):\n with _GatewayDirectHarness() as h:\n counts = h.tree[\"session\"][\"counts\"]\n self.assertEqual(counts[\"session_shape\"],\n \"interactions_not_materialized_yet\")\n self.assertEqual(counts[\"interactions_total\"], 0)\n self.assertEqual(counts[\"steps_total\"], 0)\n self.assertEqual(counts[\"gateway_requests\"], 2)\n\n\n# -----------------------------------------------------------------------------\n# render_dc on the gateway-direct tree\n# -----------------------------------------------------------------------------\n\n\nclass GatewayDirectRenderTests(unittest.TestCase):\n \"\"\"The gateway-direct render branch is short — covers identity,\n lag banner, and gateway-chain table. Drives it end-to-end.\"\"\"\n\n def test_render_emits_lag_banner(self):\n import render_dc # noqa: WPS433\n with _GatewayDirectHarness() as h:\n md = render_dc.render(h.tree, manifest=None, session_dir=h.sdir)\n # _STDM_LAG_NOTE is referenced in the gateway-direct render path\n # — must surface as visible text. We assert on the substring\n # \"STDM\" since the exact phrasing may evolve.\n self.assertIn(\"STDM\", md)\n\n def test_render_lists_both_gateway_requests(self):\n import render_dc # noqa: WPS433\n with _GatewayDirectHarness() as h:\n md = render_dc.render(h.tree, manifest=None, session_dir=h.sdir)\n # Gateway-direct render truncates IDs to 8-char prefixes — assert\n # on the prefix substring + on the prompt-template name (verbatim\n # in the per-call detail block).\n self.assertIn(IDS.GW_REQ_DECLARED[:8], md)\n self.assertIn(IDS.GW_REQ_WINDOW[:8], md)\n self.assertIn(\"AiCopilot__ReactInitialPrompt\", md)\n self.assertIn(\"AiCopilot__PromptTemplateGenerationsInvocable\", md)\n\n def test_render_includes_session_id(self):\n import render_dc # noqa: WPS433\n with _GatewayDirectHarness() as h:\n md = render_dc.render(h.tree, manifest=None, session_dir=h.sdir)\n self.assertIn(IDS.SID, md)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":6566,"content_sha256":"a3e01084234a808d394a79c3a5fc84600659f00a7d8fd4eb82f0b43bac2e397c"},{"filename":"scripts/tests/test_assemble_dc_gateway_direct.py","content":"\"\"\"Tests for ``assemble_dc._assemble_gateway_direct``.\n\nExercises the ``interactions_not_materialized_yet`` path: session row\nresolved, gateway_requests populated, interactions/steps empty. Builds a\nminimal rows-dict fixture and calls the private assembler directly — the\nfunction is pure over (sid, rows, manifest, parse_warnings), so no disk\nfixtures are needed.\n\nScope:\n - ``_source == \"gateway_direct\"`` sentinel\n - ``session.interactions == []`` (downstream consumers no-op)\n - ``session.gateway_chain`` length == fixture's gateway_requests\n - each gateway_chain entry has a populated ``response.timestamp``\n - finish_reason is lifted out of the HTML-escaped parameters JSON\n - model / provider / prompt_template_dev_name / token counts populated\n\"\"\"\nfrom __future__ import annotations\n\nimport unittest\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nfrom assemble_dc import _assemble_gateway_direct # type: ignore\n\n\n_SID = \"01912345-0000-7000-8000-000000000001\"\n\n\ndef _gw_request(req_id: str, ts: str, model: str, tokens: tuple) -> dict:\n \"\"\"Shape mirrors GenAIGatewayRequest__dlm rows as fetched.\"\"\"\n prompt_tok, completion_tok, total_tok = tokens\n return {\n \"gatewayRequestId__c\": req_id,\n \"sf__Id\": f\"sf-{req_id}\",\n \"sessionId__c\": f'\"{_SID}\"',\n \"timestamp__c\": ts,\n \"model__c\": model,\n \"provider__c\": \"openai\",\n \"promptTemplateDevName__c\": \"AiCopilot__ReactTopicPrompt\",\n \"feature__c\": \"plannerservice\",\n \"promptTokens__c\": prompt_tok,\n \"completionTokens__c\": completion_tok,\n \"totalTokens__c\": total_tok,\n \"prompt__c\": f\"This is the prompt body for {req_id}.\",\n }\n\n\ndef _gw_response(req_id: str, resp_id: str, ts: str, finish: str) -> dict:\n \"\"\"Shape mirrors GenAIGatewayResponse__dlm rows as fetched.\n\n parameters__c is HTML-escaped JSON with the finish_reason value\n wrapped in escaped quotes — the exact on-wire shape the live fetcher\n produces. Mirrors the fixture used elsewhere in the test suite.\n \"\"\"\n # Build the raw JSON first, then HTML-escape the quotes the same way\n # the gateway serializer does.\n raw = '{\"finish_reason\":\"\\\\\"' + finish + '\\\\\"\"}'\n escaped = raw.replace('\"', \""\")\n return {\n \"generationResponseId__c\": resp_id,\n \"generationRequestId__c\": req_id,\n \"parameters__c\": escaped,\n \"timestamp__c\": ts,\n }\n\n\ndef _tag(req_id: str, key: str, value: str) -> dict:\n return {\n \"id__c\": f\"tag-{req_id}-{key}\",\n \"parent__c\": req_id,\n \"tag__c\": key,\n \"tagValue__c\": value,\n }\n\n\ndef _md(req_id: str, mtype: str, payload: str) -> dict:\n return {\n \"id__c\": f\"md-{req_id}\",\n \"parent__c\": req_id,\n \"metadataType__c\": mtype,\n \"metadata__c\": payload,\n }\n\n\ndef _make_rows() -> dict:\n session_row = {\n \"ssot__Id__c\": _SID,\n \"ssot__StartTimestamp__c\": \"2026-05-03T10:00:00.000Z\",\n \"ssot__EndTimestamp__c\": None,\n \"ssot__AiAgentChannelType__c\": \"EmbeddedMessaging\",\n \"ssot__AiAgentSessionEndType__c\": None,\n }\n return {\n \"sessions\": [session_row],\n \"participants\": [],\n \"messages\": [],\n \"moments\": [],\n \"interactions\": [],\n \"steps\": [],\n \"generations\": [],\n \"gateway_requests\": [\n _gw_request(\"gw-1\", \"2026-05-03T10:00:01.000Z\", \"gpt-4o\", (100, 50, 150)),\n _gw_request(\"gw-2\", \"2026-05-03T10:00:05.000Z\", \"gpt-4o\", (200, 80, 280)),\n _gw_request(\"gw-3\", \"2026-05-03T10:00:10.000Z\", \"gpt-4o-mini\", (50, 30, 80)),\n ],\n \"gateway_responses\": [\n _gw_response(\"gw-1\", \"resp-1\", \"2026-05-03T10:00:02.500Z\", \"stop\"),\n _gw_response(\"gw-2\", \"resp-2\", \"2026-05-03T10:00:06.000Z\", \"tool_calls\"),\n _gw_response(\"gw-3\", \"resp-3\", \"2026-05-03T10:00:11.000Z\", \"stop\"),\n ],\n \"gateway_request_tags\": [\n _tag(\"gw-1\", \"bot_id\", \"0Xx000000000001\"),\n _tag(\"gw-2\", \"feature\", \"plannerservice\"),\n ],\n \"gateway_request_metadata\": [\n _md(\"gw-2\", \"ToolCall\", '{\"tool\":\"apex\"}'),\n ],\n \"gateway_request_llm\": [],\n \"content_quality\": [],\n \"content_category\": [],\n \"feedback\": [],\n \"feedback_details\": [],\n \"gateway_records\": [],\n \"tag_associations\": [],\n \"tag_definitions\": [],\n \"tag_definition_associations\": [],\n \"tags\": [],\n \"app_generation\": [],\n \"telemetry_spans\": [],\n \"moment_interactions\": [],\n }\n\n\ndef _make_manifest() -> dict:\n return {\n \"session_id\": _SID,\n \"org_alias\": \"test-org\",\n \"instance_url\": \"https://example.my.salesforce.com\",\n \"org_id_15\": \"00DXX0000000001\",\n \"agent_api_name\": \"My_Agent\",\n \"agent_version\": \"v1\",\n \"session_shape\": \"interactions_not_materialized_yet\",\n \"queries\": [],\n }\n\n\nclass AssembleGatewayDirectTests(unittest.TestCase):\n\n def setUp(self):\n self.rows = _make_rows()\n self.manifest = _make_manifest()\n self.tree = _assemble_gateway_direct(\n _SID, self.rows, self.manifest, parse_warnings=[])\n\n def test_source_sentinel(self):\n \"\"\"The render layer keys off this exact string.\"\"\"\n self.assertEqual(self.tree[\"_source\"], \"gateway_direct\")\n\n def test_interactions_is_explicit_empty_list(self):\n \"\"\"Downstream consumers walk session.interactions — must exist + be empty.\"\"\"\n self.assertEqual(self.tree[\"session\"][\"interactions\"], [])\n\n def test_gateway_chain_length_matches_input(self):\n self.assertEqual(len(self.tree[\"session\"][\"gateway_chain\"]), 3)\n\n def test_each_chain_entry_has_response_timestamp(self):\n \"\"\"Join via generationRequestId__c must populate response.timestamp on each.\"\"\"\n for call in self.tree[\"session\"][\"gateway_chain\"]:\n with self.subTest(req_id=call.get(\"gateway_request_id\")):\n self.assertIsNotNone(call.get(\"response\"))\n self.assertIsNotNone(call[\"response\"].get(\"timestamp\"))\n\n def test_finish_reason_parsed(self):\n \"\"\"HTML-escaped, quote-wrapped finish_reason must decode to bare string.\"\"\"\n reasons = [c[\"response\"][\"finish_reason\"]\n for c in self.tree[\"session\"][\"gateway_chain\"]]\n self.assertEqual(reasons, [\"stop\", \"tool_calls\", \"stop\"])\n\n def test_chain_sorted_by_timestamp(self):\n \"\"\"Deterministic ordering — required for byte-identical output across runs.\"\"\"\n timestamps = [c[\"timestamp\"] for c in self.tree[\"session\"][\"gateway_chain\"]]\n self.assertEqual(timestamps, sorted(timestamps))\n\n def test_model_and_tokens_harvested(self):\n first = self.tree[\"session\"][\"gateway_chain\"][0]\n self.assertEqual(first[\"model\"], \"gpt-4o\")\n self.assertEqual(first[\"provider\"], \"openai\")\n self.assertEqual(first[\"prompt_template_dev_name\"],\n \"AiCopilot__ReactTopicPrompt\")\n self.assertEqual(first[\"prompt_tokens\"], 100)\n self.assertEqual(first[\"total_tokens\"], 150)\n\n def test_prompt_text_untruncated_on_disk(self):\n \"\"\"Assembler stores the full prompt — the 64 KB cap is render-only.\"\"\"\n for call in self.tree[\"session\"][\"gateway_chain\"]:\n self.assertIn(\"This is the prompt body for\",\n call.get(\"prompt_text\") or \"\")\n\n def test_identity_block_present(self):\n \"\"\"Top-level identity must carry (org_id_15, agent_api_name, agent_version).\"\"\"\n self.assertEqual(self.tree[\"identity\"][\"org_id_15\"], \"00DXX0000000001\")\n self.assertEqual(self.tree[\"identity\"][\"agent_api_name\"], \"My_Agent\")\n self.assertEqual(self.tree[\"identity\"][\"agent_version\"], \"v1\")\n\n def test_stdm_lag_note_on_session(self):\n note = self.tree[\"session\"].get(\"_stdm_lag_note\") or \"\"\n self.assertIn(\"materialize on a separate cadence\", note)\n\n def test_counts_reflect_input_rows(self):\n counts = self.tree[\"session\"][\"counts\"]\n self.assertEqual(counts[\"gateway_requests\"], 3)\n self.assertEqual(counts[\"gateway_responses\"], 3)\n self.assertEqual(counts[\"interactions_total\"], 0)\n self.assertEqual(counts[\"steps_total\"], 0)\n self.assertEqual(counts[\"session_shape\"],\n \"interactions_not_materialized_yet\")\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8460,"content_sha256":"49ec856e343a77b657b941a53d80434c300b9e57f2c5e44e080929ab26c928d0"},{"filename":"scripts/tests/test_assemble_dc_helpers.py","content":"\"\"\"Tests for ``assemble_dc`` pure helpers — independent of disk fixtures.\n\nComplements ``test_assemble_dc_gateway_direct.py`` (binding chain) by\nhitting the small, mechanical helpers that bridge raw DC rows and the\nassembled tree:\n\n- ``_clean`` NOT_SET sentinel collapse\n- ``_harvest_str`` unescape + quote-strip + UNSET_VALUE\n- ``_ts`` ISO-8601 → datetime; NOT_SET / non-string → None\n- ``_index_unique`` PK dedup with first-write-wins + collision record\n- ``_groupby`` group rows by FK, dropping NOT_SET keys\n- ``_extract_trace_id`` primary col with HTML-escaped fallback\n- ``_tier`` ACTION \u003c TOPIC \u003c GUARDRAIL \u003c other\n- ``_window_contains`` half-open / closed timestamp interval semantics\n- ``_load`` disk read with malformed-json tolerance\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport unittest\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport assemble_dc # type: ignore\n\n\n# -----------------------------------------------------------------------------\n# _clean — NOT_SET → None\n# -----------------------------------------------------------------------------\n\n\nclass CleanTests(unittest.TestCase):\n\n def test_not_set_token_returns_none(self):\n self.assertIsNone(assemble_dc._clean(\"NOT_SET\"))\n\n def test_empty_string_returns_none(self):\n self.assertIsNone(assemble_dc._clean(\"\"))\n\n def test_actual_value_passes_through(self):\n self.assertEqual(assemble_dc._clean(\"real value\"), \"real value\")\n self.assertEqual(assemble_dc._clean(42), 42)\n\n\n# -----------------------------------------------------------------------------\n# _harvest_str — unescape + quote-strip + UNSET_VALUE collapse\n# -----------------------------------------------------------------------------\n\n\nclass HarvestStrTests(unittest.TestCase):\n\n def test_none_returns_none(self):\n self.assertIsNone(assemble_dc._harvest_str(None))\n\n def test_unescapes_html_entities(self):\n self.assertEqual(\n assemble_dc._harvest_str(\""0Xx000"\"),\n \"0Xx000\",\n )\n\n def test_strips_outer_double_quotes(self):\n self.assertEqual(assemble_dc._harvest_str('\"value\"'), \"value\")\n\n def test_unset_value_collapses_to_none(self):\n self.assertIsNone(assemble_dc._harvest_str(\"UNSET_VALUE\"))\n\n def test_not_set_collapses_to_none(self):\n self.assertIsNone(assemble_dc._harvest_str(\"NOT_SET\"))\n\n def test_empty_string_collapses_to_none(self):\n self.assertIsNone(assemble_dc._harvest_str(\"\"))\n\n\n# -----------------------------------------------------------------------------\n# _ts — ISO-8601 timestamp parsing\n# -----------------------------------------------------------------------------\n\n\nclass TsTests(unittest.TestCase):\n\n def test_parses_iso_z_timestamp(self):\n out = assemble_dc._ts(\"2026-04-22T10:00:00Z\")\n self.assertEqual(out, datetime(2026, 4, 22, 10, 0, 0, tzinfo=timezone.utc))\n\n def test_parses_iso_offset_timestamp(self):\n out = assemble_dc._ts(\"2026-04-22T10:00:00+00:00\")\n self.assertEqual(out, datetime(2026, 4, 22, 10, 0, 0, tzinfo=timezone.utc))\n\n def test_returns_none_for_non_string(self):\n self.assertIsNone(assemble_dc._ts(None))\n self.assertIsNone(assemble_dc._ts(12345))\n\n def test_returns_none_for_not_set(self):\n self.assertIsNone(assemble_dc._ts(\"NOT_SET\"))\n self.assertIsNone(assemble_dc._ts(\"\"))\n\n def test_returns_none_for_unparseable(self):\n self.assertIsNone(assemble_dc._ts(\"not-a-timestamp\"))\n\n\n# -----------------------------------------------------------------------------\n# _index_unique\n# -----------------------------------------------------------------------------\n\n\nclass IndexUniqueTests(unittest.TestCase):\n\n def test_builds_dict_keyed_by_field(self):\n rows = [{\"id\": \"a\", \"v\": 1}, {\"id\": \"b\", \"v\": 2}]\n out = assemble_dc._index_unique(rows, \"id\", [], dmo_label=\"t\")\n self.assertEqual(set(out.keys()), {\"a\", \"b\"})\n\n def test_first_write_wins_on_collision(self):\n rows = [{\"id\": \"a\", \"v\": 1}, {\"id\": \"a\", \"v\": 2}]\n collisions: list[dict] = []\n out = assemble_dc._index_unique(rows, \"id\", collisions, dmo_label=\"dmo1\")\n self.assertEqual(out[\"a\"][\"v\"], 1)\n self.assertEqual(len(collisions), 1)\n self.assertEqual(collisions[0][\"dmo\"], \"dmo1\")\n self.assertEqual(collisions[0][\"key\"], \"a\")\n\n def test_skips_NOT_SET_keys(self):\n rows = [{\"id\": \"NOT_SET\", \"v\": 1}, {\"id\": \"\", \"v\": 2}, {\"id\": \"a\", \"v\": 3}]\n out = assemble_dc._index_unique(rows, \"id\", [], dmo_label=\"t\")\n self.assertEqual(set(out.keys()), {\"a\"})\n\n\n# -----------------------------------------------------------------------------\n# _groupby\n# -----------------------------------------------------------------------------\n\n\nclass GroupbyTests(unittest.TestCase):\n\n def test_groups_rows_by_key(self):\n rows = [{\"k\": \"a\", \"v\": 1}, {\"k\": \"b\", \"v\": 2}, {\"k\": \"a\", \"v\": 3}]\n out = assemble_dc._groupby(rows, \"k\")\n self.assertEqual([r[\"v\"] for r in out[\"a\"]], [1, 3])\n self.assertEqual([r[\"v\"] for r in out[\"b\"]], [2])\n\n def test_skips_NOT_SET_keys(self):\n rows = [{\"k\": \"a\", \"v\": 1}, {\"k\": \"NOT_SET\", \"v\": 2}]\n out = assemble_dc._groupby(rows, \"k\")\n self.assertNotIn(\"NOT_SET\", out)\n self.assertIn(\"a\", out)\n\n\n# -----------------------------------------------------------------------------\n# _extract_trace_id\n# -----------------------------------------------------------------------------\n\n\nclass ExtractTraceIdTests(unittest.TestCase):\n\n def test_uses_primary_column_when_populated(self):\n out = assemble_dc._extract_trace_id({\"ssot__TelemetryTraceId__c\": \"abc\"})\n self.assertEqual(out, \"abc\")\n\n def test_falls_back_to_html_escaped_attribute_text(self):\n out = assemble_dc._extract_trace_id({\n \"ssot__TelemetryTraceId__c\": \"\",\n \"ssot__AttributeText__c\": '"internalTraceId":"deadbeef"',\n })\n self.assertEqual(out, \"deadbeef\")\n\n def test_returns_none_when_no_trace_id_anywhere(self):\n out = assemble_dc._extract_trace_id({\n \"ssot__TelemetryTraceId__c\": \"\",\n \"ssot__AttributeText__c\": \"no trace here\",\n })\n self.assertIsNone(out)\n\n def test_returns_none_for_NOT_SET(self):\n out = assemble_dc._extract_trace_id({\n \"ssot__TelemetryTraceId__c\": \"NOT_SET\",\n \"ssot__AttributeText__c\": \"\",\n })\n self.assertIsNone(out)\n\n\n# -----------------------------------------------------------------------------\n# _tier — ACTION \u003c TOPIC \u003c GUARDRAIL \u003c other\n# -----------------------------------------------------------------------------\n\n\nclass TierTests(unittest.TestCase):\n\n def test_action_step_is_lowest_tier(self):\n self.assertLess(\n assemble_dc._tier(\"ACTION_STEP\"),\n assemble_dc._tier(\"TOPIC_STEP\"),\n )\n\n def test_topic_step_outranks_guardrails(self):\n self.assertLess(\n assemble_dc._tier(\"TOPIC_STEP\"),\n assemble_dc._tier(\"TRUST_GUARDRAILS_STEP\"),\n )\n\n def test_unknown_step_type_falls_to_end(self):\n self.assertEqual(\n assemble_dc._tier(\"MYSTERY\"),\n len(assemble_dc._TIER_ORDER),\n )\n\n\n# -----------------------------------------------------------------------------\n# _window_contains\n# -----------------------------------------------------------------------------\n\n\nclass WindowContainsTests(unittest.TestCase):\n\n def _ts(self, h, m, s):\n return datetime(2026, 4, 22, h, m, s, tzinfo=timezone.utc)\n\n def test_in_range_inclusive_of_start_and_end(self):\n gw = self._ts(10, 0, 5)\n self.assertTrue(\n assemble_dc._window_contains(gw, self._ts(10, 0, 0), self._ts(10, 0, 10))\n )\n\n def test_open_ended_when_end_is_none(self):\n gw = self._ts(11, 0, 0)\n self.assertTrue(\n assemble_dc._window_contains(gw, self._ts(10, 0, 0), None)\n )\n\n def test_returns_false_when_start_is_none(self):\n gw = self._ts(10, 0, 0)\n self.assertFalse(\n assemble_dc._window_contains(gw, None, self._ts(11, 0, 0))\n )\n\n def test_returns_false_when_before_start(self):\n gw = self._ts(9, 0, 0)\n self.assertFalse(\n assemble_dc._window_contains(gw, self._ts(10, 0, 0), self._ts(11, 0, 0))\n )\n\n def test_returns_false_when_after_end(self):\n gw = self._ts(12, 0, 0)\n self.assertFalse(\n assemble_dc._window_contains(gw, self._ts(10, 0, 0), self._ts(11, 0, 0))\n )\n\n\n# -----------------------------------------------------------------------------\n# _load — disk read with tolerance\n# -----------------------------------------------------------------------------\n\n\nclass LoadTests(unittest.TestCase):\n\n def test_returns_empty_list_when_file_missing(self):\n with TemporaryDirectory() as t:\n warnings: list[str] = []\n self.assertEqual(\n assemble_dc._load(Path(t), \"missing\", warnings), []\n )\n # Missing file is normal — no warning recorded.\n self.assertEqual(warnings, [])\n\n def test_returns_loaded_rows_when_file_valid(self):\n with TemporaryDirectory() as t:\n tmp = Path(t)\n (tmp / \"dc.sessions.json\").write_text(json.dumps([{\"a\": 1}, {\"a\": 2}]))\n warnings: list[str] = []\n out = assemble_dc._load(tmp, \"sessions\", warnings)\n self.assertEqual(out, [{\"a\": 1}, {\"a\": 2}])\n self.assertEqual(warnings, [])\n\n def test_returns_empty_list_and_records_warning_on_malformed_json(self):\n with TemporaryDirectory() as t:\n tmp = Path(t)\n (tmp / \"dc.broken.json\").write_text(\"\u003c\u003c\u003cnot json>>>\")\n warnings: list[str] = []\n out = assemble_dc._load(tmp, \"broken\", warnings)\n self.assertEqual(out, [])\n self.assertEqual(warnings, [\"broken\"])\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10248,"content_sha256":"42e078fb0c1ef9a95b720883bdaed6f483d55771a447c018a1cdf4743c0f37ce"},{"filename":"scripts/tests/test_assemble_dc_integration.py","content":"\"\"\"End-to-end integration tests for ``assemble_dc.assemble``.\n\nDrives the full assembler against the synthetic session fixture under\n``tests/fixtures/synthetic_session.py``. Each test materializes the\nfixture into a tmp DATA_ROOT, calls ``assemble_dc.assemble(SID)``, and\nasserts on the resulting tree.\n\nThese tests cover the orchestration paths that the helper-only\n``test_assemble_dc_helpers.py`` doesn't touch:\n\n- declared Step → Generation → Response → Request chain\n- timestamp-window fallback for an un-declared gateway request\n- trace_id extraction from HTML-escaped AttributeText\n- session-identity resolution from AGENT participant\n- malformed dc.\u003cname>.json → parse_warnings recorded, fixture survives\n- minimal-tree path when sessions[] is empty (session_not_found)\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport unittest\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport assemble_dc # type: ignore\nfrom config import paths # type: ignore\nfrom .fixtures.synthetic_session import ( # type: ignore\n IDS, make_rows, write_to_disk, session_dir_for,\n)\n\n\n# -----------------------------------------------------------------------------\n# Helpers\n# -----------------------------------------------------------------------------\n\n\nclass _AssembleHarness:\n \"\"\"Context-manager that builds a tmp DATA_ROOT, materializes the\n fixture, patches ``paths.DATA_ROOT``, and yields ``(tree, sdir)``.\"\"\"\n\n def __init__(self, mutate_files=None):\n self.mutate_files = mutate_files\n\n def __enter__(self):\n self._tmp = TemporaryDirectory()\n self._tmpdir = Path(self._tmp.name)\n sdir = write_to_disk(self._tmpdir)\n if self.mutate_files:\n self.mutate_files(sdir)\n self._patch = mock.patch.object(paths, \"DATA_ROOT\", self._tmpdir)\n self._patch.start()\n self.tree, self.sdir = assemble_dc.assemble(IDS.SID)\n return self\n\n def __exit__(self, *exc):\n self._patch.stop()\n self._tmp.cleanup()\n\n\ndef _turn(tree: dict) -> dict:\n return next(\n iv for iv in tree[\"session\"][\"interactions\"]\n if iv.get(\"type\") == \"TURN\"\n )\n\n\n# -----------------------------------------------------------------------------\n# Top-level shape\n# -----------------------------------------------------------------------------\n\n\nclass AssembleTreeShapeTests(unittest.TestCase):\n\n def test_tree_top_level_keys(self):\n with _AssembleHarness() as h:\n self.assertIn(\"identity\", h.tree)\n self.assertIn(\"session\", h.tree)\n self.assertIn(\"catalog\", h.tree)\n\n def test_session_dir_path_is_under_data_root(self):\n with _AssembleHarness() as h:\n # On macOS the tmp path resolves through /private/var → /var\n # (or vice versa). Compare via .resolve() so the symlink\n # discrepancy doesn't fail the assertion.\n expected = session_dir_for(h._tmpdir).resolve()\n self.assertEqual(h.sdir.resolve(), expected)\n\n def test_three_interactions_in_chronological_order(self):\n with _AssembleHarness() as h:\n ivs = h.tree[\"session\"][\"interactions\"]\n types = [iv[\"type\"] for iv in ivs]\n self.assertEqual(types, [\"SESSION_START\", \"TURN\", \"SESSION_END\"])\n\n def test_turn_has_three_steps_in_order(self):\n with _AssembleHarness() as h:\n turn = _turn(h.tree)\n names = [s.get(\"name\") for s in turn[\"steps\"]]\n # Step names from the fixture's ssot__Name__c column\n self.assertIn(\"TopicSelection\", names)\n self.assertIn(\"PrimaryAction\", names)\n self.assertIn(\"FinalGuard\", names)\n\n def test_turn_has_two_messages_user_and_agent(self):\n with _AssembleHarness() as h:\n turn = _turn(h.tree)\n self.assertEqual(len(turn[\"messages\"]), 2)\n\n\n# -----------------------------------------------------------------------------\n# Identity resolution\n# -----------------------------------------------------------------------------\n\n\nclass AssembleIdentityTests(unittest.TestCase):\n\n def test_identity_extracted_from_agent_participant(self):\n with _AssembleHarness() as h:\n ident = h.tree[\"identity\"]\n self.assertEqual(ident[\"org_id_15\"], IDS.ORG_ID_15)\n self.assertEqual(ident[\"agent_api_name\"], IDS.AGENT_API)\n self.assertEqual(ident[\"agent_version\"], IDS.AGENT_VERSION)\n\n\n# -----------------------------------------------------------------------------\n# Trace_id extraction (HTML-escaped fallback)\n# -----------------------------------------------------------------------------\n\n\nclass AssembleTraceIdTests(unittest.TestCase):\n\n def test_turn_trace_id_pulled_from_html_escaped_attribute_text(self):\n with _AssembleHarness() as h:\n turn = _turn(h.tree)\n # The fixture intentionally puts trace_id ONLY in AttributeText\n # (HTML-escaped JSON); the primary TelemetryTraceId__c column is\n # empty. This exercises the fallback path in assemble_dc.\n self.assertEqual(turn[\"trace_id\"], IDS.TRACE_ID)\n\n\n# -----------------------------------------------------------------------------\n# Gateway binding (declared chain + timestamp-window fallback)\n# -----------------------------------------------------------------------------\n\n\nclass AssembleGatewayBindingTests(unittest.TestCase):\n\n def test_action_step_has_declared_gateway_request(self):\n with _AssembleHarness() as h:\n turn = _turn(h.tree)\n action_step = next(\n s for s in turn[\"steps\"] if s.get(\"name\") == \"PrimaryAction\"\n )\n gw = action_step[\"gateway_request\"]\n self.assertIsNotNone(gw)\n self.assertEqual(gw[\"binding_method\"], \"declared\")\n self.assertEqual(gw[\"gateway_request_id\"], IDS.GW_REQ_DECLARED)\n self.assertEqual(gw[\"model\"], \"gpt-4o\")\n self.assertEqual(gw[\"total_tokens\"], 1250)\n\n def test_topic_and_guardrail_steps_have_no_gateway_binding(self):\n with _AssembleHarness() as h:\n turn = _turn(h.tree)\n for s in turn[\"steps\"]:\n if s.get(\"name\") in (\"TopicSelection\", \"FinalGuard\"):\n self.assertIsNone(s.get(\"gateway_request\"))\n\n def test_timestamp_window_pass_picks_up_undeclared_gateway_request(self):\n with _AssembleHarness() as h:\n turn = _turn(h.tree)\n ts_calls = turn.get(\"timestamp_bound_gateway_calls\", [])\n # Exactly one gateway request lives in the TURN window without a\n # declared binding (GW_REQ_WINDOW). The declared one is consumed\n # by step-action and is excluded from the ts-window pass.\n self.assertEqual(len(ts_calls), 1)\n self.assertEqual(\n ts_calls[0][\"gateway_request_id\"], IDS.GW_REQ_WINDOW,\n )\n\n def test_declared_response_carries_finish_reason(self):\n with _AssembleHarness() as h:\n turn = _turn(h.tree)\n action_step = next(\n s for s in turn[\"steps\"] if s.get(\"name\") == \"PrimaryAction\"\n )\n # parameters__c is HTML-escaped JSON in the fixture; the\n # renderer is responsible for decoding, but the raw string\n # must be preserved unchanged in the assembled tree.\n params = action_step[\"gateway_request\"][\"response\"][\"parameters__c\"]\n self.assertIn(\"finish_reason\", params)\n self.assertIn(\""\", params)\n\n\n# -----------------------------------------------------------------------------\n# Malformed-input tolerance (parse_warnings)\n# -----------------------------------------------------------------------------\n\n\nclass AssembleParseWarningsTests(unittest.TestCase):\n\n def test_malformed_dc_file_recorded_in_parse_warnings(self):\n def corrupt_messages(sdir: Path) -> None:\n (sdir / \"dc.messages.json\").write_text(\"\u003c\u003c\u003cnot json>>>\")\n\n with _AssembleHarness(mutate_files=corrupt_messages) as h:\n # parse_warnings lives on session.counts (cf. _build_counts).\n warnings = h.tree[\"session\"][\"counts\"].get(\"parse_warnings\") or []\n self.assertIn(\"messages\", warnings)\n\n\n# -----------------------------------------------------------------------------\n# session_not_found short-circuit\n# -----------------------------------------------------------------------------\n\n\nclass AssembleSessionNotFoundTests(unittest.TestCase):\n\n def test_empty_sessions_returns_minimal_tree(self):\n def empty_sessions(sdir: Path) -> None:\n (sdir / \"dc.sessions.json\").write_text(\"[]\")\n\n with _AssembleHarness(mutate_files=empty_sessions) as h:\n tree = h.tree\n # Minimal-tree shape signaled by session_shape == 'session_not_found'\n # in the manifest section, plus an empty interactions list.\n self.assertEqual(tree[\"session\"].get(\"interactions\", []), [])\n\n\n# -----------------------------------------------------------------------------\n# Public API path: tree dict is JSON-serializable\n# -----------------------------------------------------------------------------\n\n\nclass AssembleSerializableTests(unittest.TestCase):\n\n def test_tree_round_trips_through_json(self):\n with _AssembleHarness() as h:\n blob = json.dumps(h.tree, default=str)\n back = json.loads(blob)\n # Sanity: re-loaded keys match originals\n self.assertEqual(set(back.keys()), set(h.tree.keys()))\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9513,"content_sha256":"39eae872889e3d3f9cd1830be49401606a3afb6305d7f4f71b267a508ef42e64"},{"filename":"scripts/tests/test_dc_and_resolve_session.py","content":"\"\"\"Tests for ``dc`` (Data Cloud transport) + ``resolve_session``\n(messaging-id → UUID resolver).\n\nBoth modules sit at the bottom of the dependency tree and have small,\nwell-defined public APIs that are mostly mockable at the subprocess /\nurllib boundary.\n\"\"\"\nfrom __future__ import annotations\n\nimport io\nimport json\nimport subprocess\nimport unittest\nimport urllib.error\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom types import SimpleNamespace\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport dc # type: ignore\nimport resolve_session # type: ignore\nfrom config import paths # type: ignore\nfrom .fixtures.synthetic_session import IDS, write_to_disk # type: ignore\n\n\n# -----------------------------------------------------------------------------\n# dc.load_sql / dc.parse\n# -----------------------------------------------------------------------------\n\n\nclass LoadSqlTests(unittest.TestCase):\n\n def test_substitutes_placeholders_and_strips(self):\n # discover_sessions has SELECT_LIST/JOINS/WHERE_CLAUSE/LIMIT placeholders.\n sql = dc.load_sql(\n \"discover_sessions\",\n SELECT_LIST=\"*\",\n JOINS=\"\",\n WHERE_CLAUSE=\"1=1\",\n LIMIT=\"10\",\n )\n self.assertIn(\"SELECT *\", sql)\n self.assertIn(\"LIMIT 10\", sql)\n\n\nclass ParseTests(unittest.TestCase):\n\n def test_returns_data_array(self):\n self.assertEqual(dc.parse({\"data\": [{\"a\": 1}]}), [{\"a\": 1}])\n\n def test_returns_empty_list_when_data_missing(self):\n self.assertEqual(dc.parse({}), [])\n\n def test_returns_empty_list_for_falsy_input(self):\n self.assertEqual(dc.parse(None), [])\n self.assertEqual(dc.parse({}), [])\n\n\n# -----------------------------------------------------------------------------\n# dc.resolve_org — sf CLI shell-out\n# -----------------------------------------------------------------------------\n\n\nclass ResolveOrgTests(unittest.TestCase):\n \"\"\"Tests for fail-fast access-token retrieval per forcedotcom/cli#3560.\n\n The token comes ONLY from ``sf org auth show-access-token --json\n --no-prompt``; ``sf org display`` supplies instanceUrl only. The\n display stub deliberately carries a non-empty ``TOKEN_FROM_DISPLAY``\n so any regression to a display-token fallback is caught.\n\n Tests mock ``subprocess.run`` with a side-effect callable that routes\n by argv shape (preflight ``--help`` / display / show-access-token)\n without spawning real processes.\n \"\"\"\n\n REDACTED_TOKEN = \"[REDACTED] Use 'sf org auth show-access-token' to view\"\n PREFLIGHT_ARGV = [\"sf\", \"org\", \"auth\", \"show-access-token\", \"--help\"]\n # Shape of real ``--help`` output (sf CLI 2.150.6).\n REAL_HELP = \"USAGE\\n $ sf org auth show-access-token -o \u003cvalue> [--json]\\n\"\n\n def setUp(self):\n # The preflight caches success process-wide; isolate every test.\n dc._show_access_token_capability_ok = False\n self.addCleanup(setattr, dc, \"_show_access_token_capability_ok\", False)\n\n def _cp(self, stdout: str, *, returncode: int = 0, stderr: str = \"\"):\n return SimpleNamespace(\n returncode=returncode, stdout=stdout, stderr=stderr,\n )\n\n def _display_payload(self, *, access_token: str = \"TOKEN_FROM_DISPLAY\") -> str:\n return json.dumps({\"result\": {\n \"instanceUrl\": \"https://example.salesforce.com\",\n \"accessToken\": access_token,\n }})\n\n def _show_token_payload(self, *, access_token: str = \"TOKEN_FROM_SHOW\") -> str:\n return json.dumps({\"result\": {\"accessToken\": access_token}})\n\n def _route(self, *, preflight_missing=False, primary_fails=False,\n primary_token=\"TOKEN_FROM_SHOW\", captured=None):\n \"\"\"Build a fake_run that routes by argv shape.\n\n - preflight_missing → ``show-access-token --help`` raises\n CalledProcessError (sf CLI lacks the command)\n - primary_fails → token call raises CalledProcessError\n (e.g. org not authenticated)\n - primary_token → accessToken returned by the token call\n - captured → optional list; each (argv, kwargs) is appended\n \"\"\"\n def fake_run(argv, **kwargs):\n if captured is not None:\n captured.append((argv, kwargs))\n if \"show-access-token\" in argv and \"--help\" in argv:\n if preflight_missing:\n raise subprocess.CalledProcessError(\n returncode=1, cmd=argv,\n stderr=\"show-access-token is not a sf command\",\n )\n return self._cp(self.REAL_HELP)\n if \"display\" in argv:\n return self._cp(self._display_payload())\n if \"show-access-token\" in argv:\n if primary_fails:\n raise subprocess.CalledProcessError(\n returncode=1, cmd=argv,\n stderr=\"NoOrgAuthenticationError\",\n )\n return self._cp(\n self._show_token_payload(access_token=primary_token),\n )\n raise AssertionError(f\"unexpected argv: {argv}\")\n return fake_run\n\n def test_primary_path_returns_show_token(self):\n \"\"\"Happy path — dedicated command returns a clean token.\"\"\"\n with mock.patch.object(dc.subprocess, \"run\", side_effect=self._route()):\n url, token = dc.resolve_org(\"my-org\")\n self.assertEqual(url, \"https://example.salesforce.com\")\n self.assertEqual(token, \"TOKEN_FROM_SHOW\")\n\n def test_primary_failure_raises_without_display_fallback(self):\n \"\"\"Token command failing is terminal — the display payload's\n accessToken must NOT be used as a fallback.\"\"\"\n with mock.patch.object(\n dc.subprocess, \"run\", side_effect=self._route(primary_fails=True),\n ):\n with self.assertRaises(SystemExit) as ctx:\n dc.resolve_org(\"my-org\")\n msg = str(ctx.exception)\n self.assertIn(\"sf org auth show-access-token\", msg)\n self.assertNotIn(\"TOKEN_FROM_DISPLAY\", msg)\n\n def test_primary_non_json_output_raises(self):\n def fake_run(argv, **kwargs):\n if \"--help\" in argv:\n return self._cp(self.REAL_HELP)\n if \"display\" in argv:\n return self._cp(self._display_payload())\n return self._cp(\"Warning: something not json\")\n\n with mock.patch.object(dc.subprocess, \"run\", side_effect=fake_run):\n with self.assertRaises(SystemExit) as ctx:\n dc.resolve_org(\"my-org\")\n self.assertIn(\"sf org auth show-access-token\", str(ctx.exception))\n\n def test_primary_redacted_raises(self):\n \"\"\"Dedicated command returns the placeholder — fail fast rather than\n handing the redaction string to downstream callers (which would\n cause INVALID_AUTH_HEADER 401 on every call).\"\"\"\n with mock.patch.object(\n dc.subprocess, \"run\",\n side_effect=self._route(primary_token=self.REDACTED_TOKEN),\n ):\n with self.assertRaises(SystemExit) as ctx:\n dc.resolve_org(\"my-org\")\n self.assertIn(\"could not retrieve a usable access token\", str(ctx.exception))\n\n def test_primary_empty_token_raises(self):\n with mock.patch.object(\n dc.subprocess, \"run\", side_effect=self._route(primary_token=\"\"),\n ):\n with self.assertRaises(SystemExit) as ctx:\n dc.resolve_org(\"my-org\")\n self.assertIn(\"could not retrieve a usable access token\", str(ctx.exception))\n\n def test_capability_preflight_failure_raises_clear_error(self):\n \"\"\"sf CLI without `sf org auth show-access-token` → actionable\n upgrade message, and the token call is never attempted.\"\"\"\n captured: list = []\n with mock.patch.object(\n dc.subprocess, \"run\",\n side_effect=self._route(preflight_missing=True, captured=captured),\n ):\n with self.assertRaises(SystemExit) as ctx:\n dc.resolve_org(\"my-org\")\n msg = str(ctx.exception)\n self.assertIn(\"missing required command\", msg)\n self.assertIn(\"sf org auth show-access-token\", msg)\n self.assertIn(\"Upgrade/reinstall Salesforce CLI\", msg)\n token_calls = [a for a, _ in captured\n if \"show-access-token\" in a and \"--no-prompt\" in a]\n self.assertEqual(token_calls, [])\n\n def test_zero_exit_without_help_signature_fails_and_is_not_cached(self):\n \"\"\"A CLI that answers an unknown command with exit 0 must NOT be\n treated as capable; the failure is not cached, so the next call\n re-probes.\"\"\"\n bogus = self._cp(\n \"\\x1b[1mUSAGE\\x1b[22m\\n $ sf [COMMAND]\\n\",\n stderr='{\"accessToken\":\"SECRET_Z\"}',\n )\n with mock.patch.object(\n dc.subprocess, \"run\", side_effect=[bogus, self._cp(self.REAL_HELP)],\n ) as run:\n with self.assertRaises(SystemExit) as ctx:\n dc._assert_show_access_token_capability()\n msg = str(ctx.exception)\n self.assertIn(\"missing required command\", msg)\n self.assertNotIn(\"SECRET_Z\", msg)\n self.assertFalse(dc._show_access_token_capability_ok)\n dc._assert_show_access_token_capability()\n self.assertEqual(run.call_count, 2)\n self.assertTrue(dc._show_access_token_capability_ok)\n\n def test_zero_exit_without_help_signature_blocks_resolve_org(self):\n captured: list = []\n route = self._route(captured=captured)\n\n def fake_run(argv, **kwargs):\n if \"--help\" in argv:\n captured.append((argv, kwargs))\n return self._cp(\"\")\n return route(argv, **kwargs)\n\n with mock.patch.object(dc.subprocess, \"run\", side_effect=fake_run):\n with self.assertRaises(SystemExit) as ctx:\n dc.resolve_org(\"my-org\")\n self.assertIn(\"lacked the command signature\", str(ctx.exception))\n self.assertEqual([a for a, _ in captured], [self.PREFLIGHT_ARGV])\n\n def test_zero_exit_unknown_command_notice_echoing_name_fails(self):\n \"\"\"Unknown-command notices contain the bare command name; only the\n USAGE form (`$ sf org auth show-access-token`) may pass.\"\"\"\n for out in (\n \"Error: Command org:auth:show-access-token not found.\",\n \"Warning: show-access-token is not a sf command.\",\n \"Did you mean org auth show-access-token? Run sf help for options.\",\n ):\n with self.subTest(out=out):\n dc._show_access_token_capability_ok = False\n with mock.patch.object(\n dc.subprocess, \"run\", return_value=self._cp(\"\", stderr=out),\n ):\n with self.assertRaises(SystemExit) as ctx:\n dc._assert_show_access_token_capability()\n self.assertIn(\"missing required command\", str(ctx.exception))\n self.assertFalse(dc._show_access_token_capability_ok)\n\n def test_real_help_signature_passes_preflight(self):\n with mock.patch.object(\n dc.subprocess, \"run\", return_value=self._cp(self.REAL_HELP),\n ):\n dc._assert_show_access_token_capability()\n self.assertTrue(dc._show_access_token_capability_ok)\n\n def test_raises_systemexit_when_sf_cli_missing(self):\n \"\"\"sf binary not on PATH — bail with the install hint.\"\"\"\n with mock.patch.object(\n dc.subprocess, \"run\", side_effect=FileNotFoundError(\"sf\"),\n ):\n with self.assertRaises(SystemExit) as ctx:\n dc.resolve_org(\"my-org\")\n self.assertIn(\"sf CLI not found\", str(ctx.exception))\n\n def test_raises_systemexit_on_display_failure(self):\n \"\"\"org_display itself failing is fatal — no instanceUrl, no recovery.\"\"\"\n err = subprocess.CalledProcessError(\n returncode=1, cmd=[\"sf\", \"org\", \"display\"], stderr=\"No AuthInfo found\",\n )\n with mock.patch.object(dc.subprocess, \"run\", side_effect=err):\n with self.assertRaises(SystemExit):\n dc.resolve_org(\"my-org\")\n\n def test_primary_path_argv_contains_show_access_token(self):\n \"\"\"Tripwire — token call MUST be `sf org auth show-access-token`\n with `--no-prompt`. Without `--no-prompt` the command blocks on a\n confirmation banner that --json doesn't suppress on its own.\"\"\"\n captured: list = []\n with mock.patch.object(\n dc.subprocess, \"run\", side_effect=self._route(captured=captured),\n ):\n dc.resolve_org(\"my-org\")\n\n argvs = [a for a, _ in captured]\n # display call is still required for non-secret metadata.\n self.assertTrue(any(\"display\" in a for a in argvs))\n primary = next(\n a for a in argvs if \"show-access-token\" in a and \"--no-prompt\" in a\n )\n self.assertIn(\"--json\", primary)\n self.assertIn(\"--target-org\", primary)\n\n def test_does_not_inject_show_secrets_env(self):\n \"\"\"Tripwire: no sf subprocess may rely on SF_TEMP_SHOW_SECRETS\n (removed from sf CLI per forcedotcom/cli#3560).\"\"\"\n captured: list = []\n with mock.patch.object(\n dc.subprocess, \"run\", side_effect=self._route(captured=captured),\n ):\n dc.resolve_org(\"my-org\")\n\n self.assertTrue(captured)\n for argv, kwargs in captured:\n env = kwargs.get(\"env\") or {}\n self.assertNotIn(\"SF_TEMP_SHOW_SECRETS\", env, msg=argv)\n\n def test_preflight_is_first_sf_call(self):\n captured: list = []\n with mock.patch.object(\n dc.subprocess, \"run\", side_effect=self._route(captured=captured),\n ):\n dc.resolve_org(\"my-org\")\n argvs = [a for a, _ in captured]\n self.assertEqual(argvs[0], self.PREFLIGHT_ARGV)\n self.assertIn(\"display\", argvs[1])\n\n def test_preflight_failure_never_invokes_display(self):\n captured: list = []\n with mock.patch.object(\n dc.subprocess, \"run\",\n side_effect=self._route(preflight_missing=True, captured=captured),\n ):\n with self.assertRaises(SystemExit):\n dc.resolve_org(\"my-org\")\n self.assertEqual([a for a, _ in captured], [self.PREFLIGHT_ARGV])\n\n def test_missing_sf_reports_preflight_error_not_display(self):\n captured: list = []\n\n def fake_run(argv, **kwargs):\n captured.append(argv)\n raise FileNotFoundError(\"sf\")\n\n with mock.patch.object(dc.subprocess, \"run\", side_effect=fake_run):\n with self.assertRaises(SystemExit) as ctx:\n dc.resolve_org(\"my-org\")\n self.assertIn(\"sf CLI not found\", str(ctx.exception))\n self.assertEqual(captured, [self.PREFLIGHT_ARGV])\n\n def test_preflight_runs_once_across_two_resolves(self):\n captured: list = []\n with mock.patch.object(\n dc.subprocess, \"run\", side_effect=self._route(captured=captured),\n ):\n dc.resolve_org(\"my-org\")\n dc.resolve_org(\"my-org\")\n argvs = [a for a, _ in captured]\n self.assertEqual(argvs.count(self.PREFLIGHT_ARGV), 1)\n self.assertEqual(sum(1 for a in argvs if \"display\" in a), 2)\n\n def test_failed_preflight_is_retried_on_next_resolve(self):\n captured: list = []\n with mock.patch.object(\n dc.subprocess, \"run\",\n side_effect=self._route(preflight_missing=True, captured=captured),\n ):\n with self.assertRaises(SystemExit):\n dc.resolve_org(\"my-org\")\n with mock.patch.object(\n dc.subprocess, \"run\", side_effect=self._route(captured=captured),\n ):\n _url, token = dc.resolve_org(\"my-org\")\n self.assertEqual(token, \"TOKEN_FROM_SHOW\")\n argvs = [a for a, _ in captured]\n self.assertEqual(argvs.count(self.PREFLIGHT_ARGV), 2)\n\n def _token_call_fails_with(self, *, stdout: str, stderr: str):\n def fake_run(argv, **kwargs):\n if \"--help\" in argv:\n return self._cp(self.REAL_HELP)\n if \"display\" in argv:\n return self._cp(self._display_payload())\n raise subprocess.CalledProcessError(\n returncode=1, cmd=argv, output=stdout, stderr=stderr,\n )\n return fake_run\n\n def test_token_call_failure_output_is_redacted(self):\n \"\"\"A failing `show-access-token --json` can echo the token on\n stdout (or a bearer header on stderr); neither may reach the exit\n message.\"\"\"\n cases = {\n \"stdout_only\": ('{\"accessToken\":\"SECRET_X\"}', \"\"),\n \"stderr_and_stdout\": (\n '{\"accessToken\":\"SECRET_X\"}',\n \"Error: request failed\\nBearer SECRET_Y\\n\",\n ),\n \"auth_header_and_qs\": (\n \"\", \"Authorization: Bearer SECRET_Y accessToken=SECRET_X\",\n ),\n }\n for label, (stdout, stderr) in cases.items():\n with self.subTest(label):\n with mock.patch.object(\n dc.subprocess, \"run\",\n side_effect=self._token_call_fails_with(\n stdout=stdout, stderr=stderr,\n ),\n ):\n with self.assertRaises(SystemExit) as ctx:\n dc.resolve_org(\"my-org\")\n msg = str(ctx.exception)\n self.assertNotIn(\"SECRET_X\", msg)\n self.assertNotIn(\"SECRET_Y\", msg)\n self.assertIn(\"\u003credacted>\", msg)\n\n def test_preflight_failure_output_is_redacted(self):\n for label, (stdout, stderr) in {\n \"stdout_only\": ('{\"accessToken\":\"SECRET_X\"}', \"\"),\n \"stderr\": (\"\", \"Bearer SECRET_Y\"),\n }.items():\n with self.subTest(label):\n dc._show_access_token_capability_ok = False\n err = subprocess.CalledProcessError(\n returncode=1, cmd=self.PREFLIGHT_ARGV,\n output=stdout, stderr=stderr,\n )\n with mock.patch.object(dc.subprocess, \"run\", side_effect=err):\n with self.assertRaises(SystemExit) as ctx:\n dc.resolve_org(\"my-org\")\n msg = str(ctx.exception)\n self.assertIn(\"missing required command\", msg)\n self.assertNotIn(\"SECRET_X\", msg)\n self.assertNotIn(\"SECRET_Y\", msg)\n\n def test_display_call_still_passes_verbose(self):\n \"\"\"org_display still includes --verbose for metadata shape stability.\"\"\"\n captured: list = []\n with mock.patch.object(\n dc.subprocess, \"run\", side_effect=self._route(captured=captured),\n ):\n dc.resolve_org(\"my-org\")\n\n display_argv = next(a for a, _ in captured if \"display\" in a)\n self.assertIn(\"--verbose\", display_argv)\n\n\n# -----------------------------------------------------------------------------\n# dc.post — HTTP path (urllib mock)\n# -----------------------------------------------------------------------------\n\n\nclass PostTests(unittest.TestCase):\n\n def _fake_response(self, body: bytes):\n # Context-manager mock for urllib.request.urlopen\n cm = mock.MagicMock()\n cm.__enter__.return_value.read.return_value = body\n cm.__exit__.return_value = False\n return cm\n\n def test_returns_rows_on_2xx(self):\n body = json.dumps({\"data\": [{\"a\": 1}, {\"a\": 2}]}).encode()\n with mock.patch.object(\n dc.urllib.request, \"urlopen\", return_value=self._fake_response(body),\n ):\n out = dc.post(\n \"SELECT 1\", \"https://x.salesforce.com\", \"TOKEN\", \"sessions\",\n )\n self.assertEqual(out, [{\"a\": 1}, {\"a\": 2}])\n\n def test_raises_dcqueryerror_with_query_name_on_http_error(self):\n # HTTPError carries (url, code, msg, hdrs, fp). We need fp.read() to\n # work — the impl calls e.read() to grab the body.\n err = urllib.error.HTTPError(\n url=\"https://x.salesforce.com\",\n code=400, msg=\"Bad Request\",\n hdrs=None,\n fp=io.BytesIO(b\"sql parse error: missing FROM\"),\n )\n with mock.patch.object(\n dc.urllib.request, \"urlopen\", side_effect=err,\n ):\n with self.assertRaises(dc.DCQueryError) as ctx:\n dc.post(\"SELECT bad\", \"https://x\", \"TOKEN\", \"sessions\")\n msg = str(ctx.exception)\n self.assertIn(\"sessions\", msg)\n self.assertIn(\"http=400\", msg)\n self.assertIn(\"sql parse error\", msg)\n\n\n# -----------------------------------------------------------------------------\n# resolve_session.is_messaging_id — pure shape check\n# -----------------------------------------------------------------------------\n\n\nclass IsMessagingIdTests(unittest.TestCase):\n\n def test_15_char_0Mw_prefix_matches(self):\n self.assertTrue(resolve_session.is_messaging_id(\"0MwTESTMSG12345\"))\n\n def test_18_char_0Mw_prefix_matches(self):\n self.assertTrue(resolve_session.is_messaging_id(\"0MwTESTMSG12345AAA\"))\n\n def test_uuid_does_not_match(self):\n # 36 chars, dashes — UUIDs never accidentally pass.\n self.assertFalse(resolve_session.is_messaging_id(IDS.SID))\n\n def test_empty_does_not_match(self):\n self.assertFalse(resolve_session.is_messaging_id(\"\"))\n\n def test_wrong_prefix_does_not_match(self):\n self.assertFalse(resolve_session.is_messaging_id(\"FOOVF00000AtTbV\"))\n\n\n# -----------------------------------------------------------------------------\n# resolve_session.resolve_from_disk — scans DATA_ROOT\n# -----------------------------------------------------------------------------\n\n\nclass ResolveFromDiskTests(unittest.TestCase):\n\n def test_uuid_input_passes_through_unchanged(self):\n self.assertEqual(\n resolve_session.resolve_from_disk(IDS.SID), IDS.SID,\n )\n\n def test_finds_uuid_when_messaging_id_in_dc_sessions(self):\n with TemporaryDirectory() as t:\n tmp = Path(t)\n write_to_disk(tmp) # synthetic fixture has the messaging id wired\n with mock.patch.object(\n resolve_session, \"DATA_ROOT\", tmp,\n ):\n out = resolve_session.resolve_from_disk(\"0MwTESTMSG12345AAA\")\n self.assertEqual(out, IDS.SID)\n\n def test_returns_none_when_messaging_id_not_present_on_disk(self):\n with TemporaryDirectory() as t:\n tmp = Path(t)\n write_to_disk(tmp)\n with mock.patch.object(\n resolve_session, \"DATA_ROOT\", tmp,\n ):\n out = resolve_session.resolve_from_disk(\"0Mw000000000000\")\n self.assertIsNone(out)\n\n def test_returns_none_when_data_root_missing(self):\n with TemporaryDirectory() as t:\n ghost = Path(t) / \"no-such\"\n with mock.patch.object(\n resolve_session, \"DATA_ROOT\", ghost,\n ):\n out = resolve_session.resolve_from_disk(\"0MwTESTMSG12345AAA\")\n self.assertIsNone(out)\n\n def test_skips_archive_dirs(self):\n # Plant a duplicate inside an \"\u003cuuid> - archive 1\" dir; the resolver\n # should ignore it. Without the skip, the extra row could trigger\n # spurious multi-match.\n with TemporaryDirectory() as t:\n tmp = Path(t)\n write_to_disk(tmp)\n archive = tmp / IDS.ORG_ID_15 / f\"{IDS.AGENT_API}__{IDS.AGENT_VERSION}\" / f\"{IDS.SID} - archive 1\"\n archive.mkdir(parents=True)\n (archive / \"dc.sessions.json\").write_text(json.dumps([{\n \"ssot__Id__c\": \"different-uuid-but-same-msg\",\n \"ssot__RelatedMessagingSessionId__c\": \"0MwTESTMSG12345AAA\",\n }]))\n with mock.patch.object(resolve_session, \"DATA_ROOT\", tmp):\n out = resolve_session.resolve_from_disk(\"0MwTESTMSG12345AAA\")\n # Resolver should return the canonical UUID, ignoring the archive\n # row. (If the archive weren't skipped, this would multi-match\n # raise.)\n self.assertEqual(out, IDS.SID)\n\n\n# -----------------------------------------------------------------------------\n# resolve_session.resolve_disk_or_live — combined path\n# -----------------------------------------------------------------------------\n\n\nclass ResolveDiskOrLiveTests(unittest.TestCase):\n\n def test_uuid_input_passes_through(self):\n self.assertEqual(\n resolve_session.resolve_disk_or_live(IDS.SID), IDS.SID,\n )\n\n def test_disk_hit_returns_uuid_without_dc_call(self):\n with TemporaryDirectory() as t:\n tmp = Path(t)\n write_to_disk(tmp)\n with mock.patch.object(resolve_session, \"DATA_ROOT\", tmp):\n with mock.patch.object(resolve_session, \"_live_lookup\") as live:\n out = resolve_session.resolve_disk_or_live(\n \"0MwTESTMSG12345AAA\", org=\"my-org\",\n )\n live.assert_not_called()\n self.assertEqual(out, IDS.SID)\n\n def test_disk_miss_no_org_raises_with_hint(self):\n with TemporaryDirectory() as t:\n tmp = Path(t)\n with mock.patch.object(resolve_session, \"DATA_ROOT\", tmp):\n with self.assertRaises(SystemExit) as ctx:\n resolve_session.resolve_disk_or_live(\n \"0MwTESTMSG12345AAA\",\n )\n self.assertIn(\"cannot resolve messaging id\", str(ctx.exception))\n\n\n# -----------------------------------------------------------------------------\n# resolve_session.resolve — live DC-backed path\n# -----------------------------------------------------------------------------\n\n\nclass ResolveLiveTests(unittest.TestCase):\n\n def test_single_row_returns_uuid(self):\n with mock.patch.object(\n resolve_session, \"_live_lookup\",\n return_value=[{\"ssot__Id__c\": \"uuid-1\"}],\n ):\n out = resolve_session.resolve(\"0MwTESTMSG12345AAA\", org=\"my-org\")\n self.assertEqual(out, \"uuid-1\")\n\n def test_zero_rows_raises(self):\n with mock.patch.object(\n resolve_session, \"_live_lookup\", return_value=[],\n ):\n with self.assertRaises(SystemExit) as ctx:\n resolve_session.resolve(\"0MwTESTMSG12345AAA\", org=\"my-org\")\n self.assertIn(\"no ssot__AIAgentSession\", str(ctx.exception))\n\n def test_multi_row_raises_with_candidate_list(self):\n rows = [\n {\"ssot__Id__c\": \"uuid-A\", \"ssot__StartTimestamp__c\": \"t\"},\n {\"ssot__Id__c\": \"uuid-B\", \"ssot__StartTimestamp__c\": \"t\"},\n ]\n with mock.patch.object(\n resolve_session, \"_live_lookup\", return_value=rows,\n ):\n with self.assertRaises(SystemExit) as ctx:\n resolve_session.resolve(\"0MwTESTMSG12345AAA\", org=\"my-org\")\n self.assertIn(\"uuid-A\", str(ctx.exception))\n self.assertIn(\"uuid-B\", str(ctx.exception))\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":27340,"content_sha256":"ccbd86459503fc3b0471a595e26b183fc82bd4d92a773863d4dd5cd16db23895"},{"filename":"scripts/tests/test_default_target_org.py","content":"\"\"\"``--org`` is optional: entry points fall back to the sf CLI default org.\n\nCovers ``dc.default_target_org`` (mocked at the subprocess boundary) and the\nthree user entry points that take ``--org`` — ``discover_sessions``,\n``fetch_dc`` and ``resolve_session`` — with and without an explicit alias.\n\n``default_target_org`` runs the show-access-token capability preflight\n(``sf org auth show-access-token --help``) before ``sf config get``, so every\nfake here routes by argv: the preflight gets real-shaped USAGE help (or a\n\"command missing\" answer), and ``sf config get`` gets the canned payload.\n\"\"\"\nfrom __future__ import annotations\n\nimport io\nimport json\nimport subprocess\nimport unittest\nfrom types import SimpleNamespace\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport dc # type: ignore\nimport discover_sessions # type: ignore\nimport fetch_dc # type: ignore\nimport resolve_session # type: ignore\nfrom .fixtures.synthetic_session import IDS # type: ignore\nfrom .test_fetch_dc_main import _MainHarness as _FetchHarness # type: ignore\n\n_CONFIG_ARGV = [\"sf\", \"config\", \"get\", \"target-org\", \"--json\"]\n_PREFLIGHT_ARGV = [\"sf\", \"org\", \"auth\", \"show-access-token\", \"--help\"]\n# Shape of real ``--help`` output (sf CLI 2.150.6).\n_REAL_HELP = \"USAGE\\n $ sf org auth show-access-token -o \u003cvalue> [--json]\\n\"\n_UPGRADE_MSG = \"Upgrade/reinstall Salesforce CLI\"\n_NO_ORG_MSG = \"no --org given and no default target org set\"\n\n\ndef _config_payload(value: str | None) -> str:\n entry = {\"name\": \"target-org\", \"location\": \"Local\", \"success\": True}\n if value is not None:\n entry[\"value\"] = value\n return json.dumps({\"status\": 0, \"result\": [entry], \"warnings\": []})\n\n\ndef _cp(stdout: str) -> SimpleNamespace:\n return SimpleNamespace(returncode=0, stdout=stdout, stderr=\"\")\n\n\ndef _sf_run(value: str | None = None, *, config=None, preflight: str = \"ok\",\n calls: list | None = None):\n \"\"\"subprocess.run stand-in routing by argv.\n\n ``preflight``: \"ok\" → real USAGE help; \"nonzero\" → the CLI rejects the\n unknown command (CalledProcessError); \"no-usage\" → exit 0 without the\n USAGE line (an old CLI echoing a not-found notice).\n ``config``: a callable/exception overriding the ``sf config get`` answer;\n default is the canned payload for ``value``. ``calls`` records argv.\n \"\"\"\n def fake_run(argv, **kwargs):\n if calls is not None:\n calls.append((argv, kwargs))\n if argv == _PREFLIGHT_ARGV:\n if preflight == \"ok\":\n return _cp(_REAL_HELP)\n if preflight == \"nonzero\":\n raise subprocess.CalledProcessError(\n returncode=127, cmd=argv,\n stderr=\"Warning: show-access-token is not a sf command.\",\n )\n if preflight == \"no-usage\":\n return _cp(\"Command org:auth:show-access-token not found.\")\n raise AssertionError(f\"bad preflight mode {preflight!r}\")\n if argv == _CONFIG_ARGV:\n if isinstance(config, BaseException):\n raise config\n if callable(config):\n return config(argv, **kwargs)\n return _cp(_config_payload(value))\n raise AssertionError(f\"unexpected argv: {argv}\")\n return fake_run\n\n\ndef _sf_config_run(value: str | None):\n \"\"\"A CLI with a working preflight whose default org is ``value``.\"\"\"\n return _sf_run(value)\n\n\n# A CLI lacking show-access-token, in both shapes the preflight rejects.\n_MISSING_CMD_MODES = (\"nonzero\", \"no-usage\")\n\n\nclass _FreshPreflight(unittest.TestCase):\n \"\"\"The preflight caches success process-wide; isolate every test.\"\"\"\n\n def setUp(self):\n dc._show_access_token_capability_ok = False\n self.addCleanup(setattr, dc, \"_show_access_token_capability_ok\", False)\n\n\n# -----------------------------------------------------------------------------\n# dc.default_target_org\n# -----------------------------------------------------------------------------\n\n\nclass DefaultTargetOrgTests(_FreshPreflight):\n\n def test_returns_configured_value_with_timeout(self):\n calls: list = []\n with mock.patch.object(\n dc.subprocess, \"run\", side_effect=_sf_run(\"my-default\", calls=calls),\n ):\n self.assertEqual(dc.default_target_org(), \"my-default\")\n self.assertEqual([a for a, _ in calls], [_PREFLIGHT_ARGV, _CONFIG_ARGV])\n config_kwargs = calls[1][1]\n self.assertTrue(config_kwargs.get(\"timeout\"))\n self.assertTrue(config_kwargs.get(\"check\"))\n\n def test_missing_show_access_token_fails_before_config_get(self):\n for mode in _MISSING_CMD_MODES:\n with self.subTest(preflight=mode):\n dc._show_access_token_capability_ok = False\n calls: list = []\n with mock.patch.object(\n dc.subprocess, \"run\",\n side_effect=_sf_run(None, preflight=mode, calls=calls),\n ):\n with self.assertRaises(SystemExit) as ctx:\n dc.default_target_org()\n msg = str(ctx.exception)\n self.assertIn(\"sf org auth show-access-token\", msg)\n self.assertIn(_UPGRADE_MSG, msg)\n self.assertNotIn(_NO_ORG_MSG, msg)\n self.assertEqual([a for a, _ in calls], [_PREFLIGHT_ARGV])\n self.assertFalse(dc._show_access_token_capability_ok)\n\n def test_preflight_cached_across_default_org_and_resolve_org(self):\n calls: list = []\n base = _sf_run(\"my-default\", calls=calls)\n\n def fake_run(argv, **kwargs):\n if argv[:3] == [\"sf\", \"org\", \"display\"]:\n calls.append((argv, kwargs))\n return _cp(json.dumps({\"result\": {\"instanceUrl\": \"https://x\"}}))\n if argv[:4] == [\"sf\", \"org\", \"auth\", \"show-access-token\"] and \"--help\" not in argv:\n calls.append((argv, kwargs))\n return _cp(json.dumps({\"result\": {\"accessToken\": \"TOK\"}}))\n return base(argv, **kwargs)\n\n with mock.patch.object(dc.subprocess, \"run\", side_effect=fake_run):\n org = dc.default_target_org()\n self.assertEqual(dc.default_target_org(), org)\n self.assertEqual(dc.resolve_org(org), (\"https://x\", \"TOK\"))\n argvs = [a for a, _ in calls]\n self.assertEqual(argvs[0], _PREFLIGHT_ARGV)\n self.assertEqual(argvs.count(_PREFLIGHT_ARGV), 1, argvs)\n\n def test_empty_result_raises_clear_error(self):\n payloads = [\n _config_payload(None), # key unset\n _config_payload(\"\"), # blank value\n json.dumps({\"status\": 0, \"result\": []}), # no entries\n json.dumps({\"status\": 0}), # no result at all\n ]\n for payload in payloads:\n with self.subTest(payload=payload):\n with mock.patch.object(\n dc.subprocess, \"run\",\n side_effect=_sf_run(config=lambda *a, _p=payload, **k: _cp(_p)),\n ):\n with self.assertRaises(SystemExit) as ctx:\n dc.default_target_org()\n msg = str(ctx.exception)\n self.assertIn(\"no --org given and no default target org set\", msg)\n self.assertIn(\"sf config set target-org \u003calias>\", msg)\n\n def test_sf_failure_raises_redacted_error(self):\n err = subprocess.CalledProcessError(\n returncode=1, cmd=_CONFIG_ARGV,\n stderr='boom Authorization: Bearer 00DSECRET!tok \"accessToken\": \"00DLEAK\"',\n )\n with mock.patch.object(dc.subprocess, \"run\", side_effect=_sf_run(config=err)):\n with self.assertRaises(SystemExit) as ctx:\n dc.default_target_org()\n msg = str(ctx.exception)\n self.assertIn(\"no --org given and no default target org set\", msg)\n self.assertIn(\"sf config get target-org\", msg)\n self.assertIn(\"boom\", msg)\n self.assertNotIn(\"00DSECRET\", msg)\n self.assertNotIn(\"00DLEAK\", msg)\n self.assertIn(\"\u003credacted>\", msg)\n\n def test_sf_missing_raises_clear_error(self):\n # No sf at all: the preflight is the first call and reports it.\n calls: list = []\n\n def fake_run(argv, **kwargs):\n calls.append(argv)\n raise FileNotFoundError(\"sf\")\n\n with mock.patch.object(dc.subprocess, \"run\", side_effect=fake_run):\n with self.assertRaises(SystemExit) as ctx:\n dc.default_target_org()\n self.assertIn(\"sf CLI not found on PATH\", str(ctx.exception))\n self.assertEqual(calls, [_PREFLIGHT_ARGV])\n\n def test_sf_vanishes_before_config_get_raises_clear_error(self):\n with mock.patch.object(\n dc.subprocess, \"run\", side_effect=_sf_run(config=FileNotFoundError(\"sf\")),\n ):\n with self.assertRaises(SystemExit) as ctx:\n dc.default_target_org()\n self.assertIn(\"sf CLI not found on PATH\", str(ctx.exception))\n\n def test_timeout_raises_clear_error(self):\n with mock.patch.object(\n dc.subprocess, \"run\",\n side_effect=_sf_run(\n config=subprocess.TimeoutExpired(cmd=_CONFIG_ARGV, timeout=30),\n ),\n ):\n with self.assertRaises(SystemExit) as ctx:\n dc.default_target_org()\n msg = str(ctx.exception)\n self.assertIn(\"`sf config get target-org` timed out\", msg)\n self.assertIn(_NO_ORG_MSG, msg)\n\n def test_non_json_output_raises_clear_error(self):\n with mock.patch.object(\n dc.subprocess, \"run\",\n side_effect=_sf_run(config=lambda *a, **k: _cp(\"Warning: not json\")),\n ):\n with self.assertRaises(SystemExit) as ctx:\n dc.default_target_org()\n self.assertIn(\"non-JSON\", str(ctx.exception))\n\n\n# -----------------------------------------------------------------------------\n# discover_sessions\n# -----------------------------------------------------------------------------\n\n\nclass DiscoverSessionsOrgDefaultTests(_FreshPreflight):\n\n def _run(self, *argv: str, run_side_effect):\n with mock.patch.object(discover_sessions.sys, \"argv\",\n [\"discover_sessions.py\", *argv]):\n with mock.patch.object(dc.subprocess, \"run\",\n side_effect=run_side_effect) as run:\n with mock.patch.object(\n discover_sessions, \"resolve_org\",\n return_value=(\"https://x\", \"T\"),\n ) as resolve:\n with mock.patch.object(discover_sessions, \"post\", return_value=[]):\n with mock.patch(\"sys.stdout\", io.StringIO()) as out:\n rc = discover_sessions.main()\n return rc, run, resolve, out.getvalue()\n\n def test_no_org_uses_default_target_org(self):\n rc, run, resolve, out = self._run(\n \"--grep\", \"refund\", \"--limit\", \"5\",\n run_side_effect=_sf_config_run(\"def-org\"),\n )\n self.assertEqual(rc, 2) # zero rows\n self.assertEqual(\n [c.args[0] for c in run.call_args_list], [_PREFLIGHT_ARGV, _CONFIG_ARGV],\n )\n resolve.assert_called_once_with(\"def-org\")\n self.assertIn(\"def-org\", out)\n\n def test_explicit_org_wins_and_helper_not_called(self):\n with mock.patch.object(discover_sessions, \"default_target_org\") as helper:\n rc, run, resolve, _ = self._run(\n \"--org\", \"explicit-org\",\n run_side_effect=AssertionError(\"sf must not be called\"),\n )\n self.assertEqual(rc, 2)\n helper.assert_not_called()\n run.assert_not_called()\n resolve.assert_called_once_with(\"explicit-org\")\n\n def test_no_org_and_no_default_exits_with_clear_message(self):\n with self.assertRaises(SystemExit) as ctx:\n self._run(run_side_effect=_sf_config_run(None))\n self.assertIn(\"no --org given and no default target org set\", str(ctx.exception))\n\n def test_no_org_on_cli_lacking_show_access_token_reports_upgrade(self):\n # No default org AND no show-access-token: the upgrade message wins,\n # `sf config get` is never run, and no org resolution is attempted.\n for mode in _MISSING_CMD_MODES:\n with self.subTest(preflight=mode):\n dc._show_access_token_capability_ok = False\n calls: list = []\n with mock.patch.object(discover_sessions.sys, \"argv\",\n [\"discover_sessions.py\"]):\n with mock.patch.object(\n dc.subprocess, \"run\",\n side_effect=_sf_run(None, preflight=mode, calls=calls),\n ):\n with mock.patch.object(discover_sessions, \"resolve_org\") as resolve:\n with self.assertRaises(SystemExit) as ctx:\n discover_sessions.main()\n msg = str(ctx.exception)\n self.assertIn(_UPGRADE_MSG, msg)\n self.assertNotIn(_NO_ORG_MSG, msg)\n self.assertNotIn(_CONFIG_ARGV, [a for a, _ in calls])\n self.assertEqual([a for a, _ in calls], [_PREFLIGHT_ARGV])\n resolve.assert_not_called()\n\n\n# -----------------------------------------------------------------------------\n# fetch_dc\n# -----------------------------------------------------------------------------\n\n\nclass FetchDcOrgDefaultTests(_FreshPreflight):\n\n _BASE = [\"fetch_dc.py\", \"--session\", IDS.SID, \"--no-assemble\", \"--no-render\"]\n\n def test_no_org_uses_default_target_org(self):\n captured = {}\n\n def capture(ctx):\n captured.update(ctx)\n ctx[\"org_id_15\"] = IDS.ORG_ID_15\n ctx[\"agent_api_name\"] = IDS.AGENT_API\n ctx[\"agent_version\"] = IDS.AGENT_VERSION\n\n with _FetchHarness():\n with mock.patch.object(fetch_dc, \"_run_waterfall\", side_effect=capture):\n with mock.patch.object(dc.subprocess, \"run\",\n side_effect=_sf_config_run(\"def-org\")) as run:\n with mock.patch.object(fetch_dc.sys, \"argv\", list(self._BASE)):\n rc = fetch_dc.main()\n fetch_dc.preflight_dc_access.assert_called_once_with(IDS.SID, \"def-org\")\n self.assertEqual(rc, 0)\n self.assertEqual(\n [c.args[0] for c in run.call_args_list], [_PREFLIGHT_ARGV, _CONFIG_ARGV],\n )\n self.assertEqual(captured[\"org_alias\"], \"def-org\")\n\n def test_explicit_org_wins_and_helper_not_called(self):\n with _FetchHarness():\n with mock.patch.object(fetch_dc, \"default_target_org\") as helper:\n with mock.patch.object(\n fetch_dc.sys, \"argv\", [*self._BASE, \"--org\", \"explicit-org\"],\n ):\n rc = fetch_dc.main()\n fetch_dc.preflight_dc_access.assert_called_once_with(\n IDS.SID, \"explicit-org\",\n )\n self.assertEqual(rc, 0)\n helper.assert_not_called()\n\n def test_no_default_routes_through_dc_access_denied(self):\n with _FetchHarness(is_tty=False):\n with mock.patch.object(dc.subprocess, \"run\",\n side_effect=_sf_config_run(None)):\n with mock.patch.object(fetch_dc.sys, \"argv\", list(self._BASE)):\n with mock.patch(\"sys.stdout\", io.StringIO()) as out:\n rc = fetch_dc.main()\n fetch_dc.preflight_dc_access.assert_not_called()\n self.assertEqual(rc, fetch_dc.EXIT_DC_ACCESS_DENIED)\n payload = json.loads(out.getvalue().strip().splitlines()[-1])\n self.assertEqual(payload[\"status\"], \"DC_ACCESS_DENIED\")\n self.assertEqual(payload[\"reason\"], \"no_org\")\n self.assertIn(\"no default target org set\", payload[\"detail\"])\n\n def test_no_org_on_cli_lacking_show_access_token_reports_upgrade(self):\n # Headless contract is unchanged (DC_ACCESS_DENIED, exit 10, same\n # reason code the explicit-org preflight failure already uses), but the\n # detail is the upgrade message and `sf config get` never runs.\n for mode in _MISSING_CMD_MODES:\n with self.subTest(preflight=mode):\n dc._show_access_token_capability_ok = False\n calls: list = []\n with _FetchHarness(is_tty=False):\n with mock.patch.object(\n dc.subprocess, \"run\",\n side_effect=_sf_run(None, preflight=mode, calls=calls),\n ):\n with mock.patch.object(fetch_dc.sys, \"argv\", list(self._BASE)):\n with mock.patch(\"sys.stdout\", io.StringIO()) as out:\n rc = fetch_dc.main()\n fetch_dc.preflight_dc_access.assert_not_called()\n self.assertEqual(rc, fetch_dc.EXIT_DC_ACCESS_DENIED)\n payload = json.loads(out.getvalue().strip().splitlines()[-1])\n self.assertEqual(payload[\"status\"], \"DC_ACCESS_DENIED\")\n self.assertIn(_UPGRADE_MSG, payload[\"detail\"])\n self.assertNotIn(_NO_ORG_MSG, payload[\"detail\"])\n self.assertEqual([a for a, _ in calls], [_PREFLIGHT_ARGV])\n\n\n# -----------------------------------------------------------------------------\n# resolve_session (live lookup only needs an org on a disk miss)\n# -----------------------------------------------------------------------------\n\n\nclass ResolveSessionOrgDefaultTests(unittest.TestCase):\n\n _MSG = \"0Mw000000000000\"\n\n def _run(self, *argv: str):\n with mock.patch.object(resolve_session.sys, \"argv\",\n [\"resolve_session.py\", \"--id\", self._MSG, *argv]):\n with mock.patch.object(resolve_session, \"resolve_from_disk\", return_value=None):\n with mock.patch.object(\n resolve_session, \"resolve\", return_value=\"resolved-uuid\",\n ) as resolve:\n with mock.patch(\"sys.stdout\", io.StringIO()) as out:\n rc = resolve_session.main()\n return rc, resolve, out.getvalue()\n\n def test_disk_miss_without_org_uses_default(self):\n with mock.patch.object(\n resolve_session, \"default_target_org\", return_value=\"def-org\",\n ) as helper:\n rc, resolve, out = self._run()\n self.assertEqual(rc, 0)\n helper.assert_called_once_with()\n resolve.assert_called_once_with(self._MSG, org=\"def-org\")\n self.assertIn(\"resolved-uuid\", out)\n\n def test_explicit_org_wins_and_helper_not_called(self):\n with mock.patch.object(resolve_session, \"default_target_org\") as helper:\n rc, resolve, _ = self._run(\"--org\", \"explicit-org\")\n self.assertEqual(rc, 0)\n helper.assert_not_called()\n resolve.assert_called_once_with(self._MSG, org=\"explicit-org\")\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":19230,"content_sha256":"e813bae32efbf99146743f3f5ecffe7ffec70392c4a76abbe05279d68d60742c"},{"filename":"scripts/tests/test_discover_sessions_grep_ci.py","content":"\"\"\"Regression guard for §16d — case-insensitive ``--grep`` default.\n\n§16d flipped the default behaviour of ``--grep`` from case-sensitive\nexact match to case-insensitive (``LOWER(...) LIKE LOWER(...)``).\nOperators almost always type the quoted phrase in whatever case they\nremember, not the case the agent rendered it in — the old default\nsilently dropped legitimate matches.\n\nThe compose_sql shape is already pinned in\n``test_discover_sessions.py`` (LOWER wrap on default, bypass on opt-in,\nescape semantics). What this file pins down is the\n**argparse → filters dict → picker header** pipeline — the integration\nseams where a refactor could:\n\n - flip the argparse default back to ``True`` (forgot ``default=False``)\n - drop the ``grep_case_sensitive`` key from the filters dict (header\n no longer warns the operator that the picker is in opt-in mode)\n - surface ``grep_case_sensitive`` even when ``--grep`` isn't set\n (false signal in the header for unrelated runs)\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport unittest\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport discover_sessions # type: ignore\n\n\nclass GrepCaseSensitiveArgparseDefaultTests(unittest.TestCase):\n \"\"\"The argparse flag must default to ``False`` so case-insensitive\n is the path of least resistance.\"\"\"\n\n def _build_parser(self) -> argparse.ArgumentParser:\n # Re-export the parser the same way main() does — we don't have\n # a parser factory, so spy on the module-level ``argparse`` to\n # capture the parser before main() invokes parse_args().\n captured: dict[str, argparse.ArgumentParser] = {}\n real_parse_args = argparse.ArgumentParser.parse_args\n\n def capture(self, *a, **kw): # type: ignore[no-redef]\n captured[\"parser\"] = self\n # Stop main() before it calls anything network-y.\n raise SystemExit(0)\n\n with mock.patch.object(argparse.ArgumentParser, \"parse_args\", capture):\n with mock.patch.object(\n discover_sessions.sys, \"argv\",\n [\"discover_sessions.py\", \"--org\", \"x\"],\n ):\n with self.assertRaises(SystemExit):\n discover_sessions.main()\n # Restore (the patch.object exit already did this, but be explicit\n # in case something held onto the bound method.)\n argparse.ArgumentParser.parse_args = real_parse_args\n return captured[\"parser\"]\n\n def test_grep_case_sensitive_default_is_false(self):\n # parse_args without --grep-case-sensitive must return False.\n parser = self._build_parser()\n ns = parser.parse_args([\"--org\", \"x\"])\n self.assertFalse(ns.grep_case_sensitive,\n \"default flipped — case-insensitive was the §16d \"\n \"decision; flipping default=False back to True \"\n \"regresses to the pre-§16d behaviour\")\n\n def test_grep_case_sensitive_opt_in_via_flag(self):\n # Pass the flag explicitly — must be True.\n parser = self._build_parser()\n ns = parser.parse_args([\"--org\", \"x\", \"--grep-case-sensitive\"])\n self.assertTrue(ns.grep_case_sensitive)\n\n\nclass FiltersDictSurfacesCaseSensitiveTests(unittest.TestCase):\n \"\"\"The filters dict feeds render_picker's header. Without a marker,\n the operator can't tell whether their grep is in case-insensitive\n (default) or case-sensitive (opt-in) mode — a silent footgun when\n a query returns zero rows in opt-in mode by accident.\"\"\"\n\n def _run_main_and_capture_filters(self, *extra_argv: str) -> dict:\n # Stub resolve_org + post so main() doesn't talk to DC, then\n # intercept render_picker to capture the filters dict main()\n # constructed.\n captured: dict[str, dict] = {}\n\n def fake_render(*, filters, **_kw):\n captured[\"filters\"] = filters\n return \"ok\"\n\n argv = [\"discover_sessions.py\", \"--org\", \"x\", *extra_argv]\n with mock.patch.object(discover_sessions.sys, \"argv\", argv), \\\n mock.patch.object(\n discover_sessions, \"resolve_org\",\n return_value=(\"https://x\", \"T\"),\n ), \\\n mock.patch.object(\n discover_sessions, \"post\",\n return_value=[{\n \"ssot__Id__c\": \"sid1\",\n \"ssot__StartTimestamp__c\": \"2026-04-22T00:00:00Z\",\n \"ssot__EndTimestamp__c\": \"2026-04-22T00:01:00Z\",\n \"ssot__AiAgentChannelType__c\": \"Builder\",\n \"ssot__AiAgentSessionEndType__c\": \"USER_ENDED\",\n }],\n ), \\\n mock.patch.object(\n discover_sessions, \"render_picker\",\n side_effect=fake_render,\n ):\n discover_sessions.main()\n return captured[\"filters\"]\n\n def test_filters_omit_case_sensitive_when_grep_unset(self):\n # No --grep at all: surfacing case-sensitive=yes in the header\n # would be a meaningless artifact (there's no grep to be\n # case-sensitive about). Contract: the value is None so\n # render_picker's `if v:` filter drops the bit.\n filters = self._run_main_and_capture_filters()\n self.assertIsNone(filters[\"grep_case_sensitive\"])\n\n def test_filters_omit_case_sensitive_when_grep_set_but_default(self):\n # --grep set, --grep-case-sensitive NOT set: default mode is\n # case-insensitive. Don't surface the marker — the absence is\n # the default and surfacing \"yes/no\" on every grep would just\n # add noise to the picker header.\n filters = self._run_main_and_capture_filters(\"--grep\", \"refund\")\n self.assertEqual(filters[\"grep\"], \"refund\")\n self.assertIsNone(filters[\"grep_case_sensitive\"])\n\n def test_filters_surface_case_sensitive_only_when_both_set(self):\n # The interesting case: operator opted in. The header MUST tell\n # them, otherwise a zero-row return looks identical to a\n # case-insensitive zero-row return.\n filters = self._run_main_and_capture_filters(\n \"--grep\", \"Refund\", \"--grep-case-sensitive\",\n )\n self.assertEqual(filters[\"grep\"], \"Refund\")\n self.assertEqual(filters[\"grep_case_sensitive\"], \"yes\")\n\n\nclass RenderPickerHeaderShowsCaseSensitiveTests(unittest.TestCase):\n \"\"\"End-to-end: when filters dict surfaces ``grep_case_sensitive``,\n render_picker must include it in the header.\"\"\"\n\n def _row(self) -> dict:\n return {\n \"ssot__Id__c\": \"sid1\",\n \"ssot__StartTimestamp__c\": \"2026-04-22T10:00:00Z\",\n \"ssot__EndTimestamp__c\": \"2026-04-22T10:00:30Z\",\n \"ssot__AiAgentChannelType__c\": \"Builder\",\n \"ssot__AiAgentSessionEndType__c\": \"USER_ENDED\",\n }\n\n def _tr(self) -> \"discover_sessions.TimeRange\":\n from datetime import datetime, timezone\n return discover_sessions.TimeRange(\n start_utc=datetime(2026, 4, 22, 0, 0, 0, tzinfo=timezone.utc),\n end_utc=datetime(2026, 4, 22, 23, 0, 0, tzinfo=timezone.utc),\n expr=\"test\", tz_name=\"UTC\",\n )\n\n def test_header_includes_case_sensitive_bit_when_set(self):\n out = discover_sessions.render_picker(\n rows=[self._row()], agent_by_sid={}, org=\"my-org\",\n tr=self._tr(),\n filters={\n \"agent\": None, \"channel\": None, \"outcome\": None,\n \"grep\": \"Refund\", \"grep_case_sensitive\": \"yes\",\n },\n composed_sql=\"\",\n )\n self.assertIn(\"grep=Refund\", out)\n self.assertIn(\"grep_case_sensitive=yes\", out)\n\n def test_header_omits_case_sensitive_bit_in_default_mode(self):\n # Default-mode grep (no opt-in): the bit must NOT appear.\n out = discover_sessions.render_picker(\n rows=[self._row()], agent_by_sid={}, org=\"my-org\",\n tr=self._tr(),\n filters={\n \"agent\": None, \"channel\": None, \"outcome\": None,\n \"grep\": \"refund\", \"grep_case_sensitive\": None,\n },\n composed_sql=\"\",\n )\n self.assertIn(\"grep=refund\", out)\n self.assertNotIn(\"grep_case_sensitive\", out)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8373,"content_sha256":"d9d5f72c5a2cd2be973b0ef9fee91b2f13a7ca68ba301cfd1bfea5b6435ab1fb"},{"filename":"scripts/tests/test_discover_sessions.py","content":"\"\"\"Tests for ``discover_sessions`` — DC-only session picker.\n\nCovers:\n- ``parse_time_expr`` pure str → TimeRange (12 supported expressions)\n- ``compose_sql`` pure args → SQL string (conditional JOINs / WHEREs)\n- ``fetch_agent_names`` DC follow-up call (mocks ``post``)\n- ``render_picker`` pure rows → markdown\n- ``main`` argv → exit code (mocks ``resolve_org`` + ``post``)\n\"\"\"\nfrom __future__ import annotations\n\nimport unittest\nfrom datetime import datetime, timedelta, timezone\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport discover_sessions # type: ignore\nfrom dc import DCQueryError # type: ignore\n\n\n# -----------------------------------------------------------------------------\n# parse_time_expr — pure\n# -----------------------------------------------------------------------------\n\n\nclass ParseTimeExprTests(unittest.TestCase):\n\n def test_default_none_returns_last_24_hours(self):\n tr = discover_sessions.parse_time_expr(None, \"UTC\")\n delta = tr.end_utc - tr.start_utc\n self.assertEqual(delta, timedelta(hours=24))\n self.assertIn(\"default\", tr.expr)\n\n def test_last_n_hours(self):\n tr = discover_sessions.parse_time_expr(\"last 2 hours\", \"UTC\")\n self.assertEqual(tr.end_utc - tr.start_utc, timedelta(hours=2))\n self.assertEqual(tr.expr, \"last 2 hours\")\n\n def test_last_n_minutes(self):\n tr = discover_sessions.parse_time_expr(\"last 30 minutes\", \"UTC\")\n self.assertEqual(tr.end_utc - tr.start_utc, timedelta(minutes=30))\n\n def test_last_n_days(self):\n tr = discover_sessions.parse_time_expr(\"last 3 days\", \"UTC\")\n self.assertEqual(tr.end_utc - tr.start_utc, timedelta(days=3))\n\n def test_last_n_bare_defaults_to_hours(self):\n # Spec: \"last 10\" is ambiguous; the regex treats unit-less as hours.\n tr = discover_sessions.parse_time_expr(\"last 10\", \"UTC\")\n self.assertEqual(tr.end_utc - tr.start_utc, timedelta(hours=10))\n\n def test_today_uses_local_tz_calendar_day(self):\n tr = discover_sessions.parse_time_expr(\"today\", \"UTC\")\n # End - start should be exactly 24 hours.\n self.assertEqual(tr.end_utc - tr.start_utc, timedelta(days=1))\n\n def test_yesterday_window_precedes_today(self):\n today = discover_sessions.parse_time_expr(\"today\", \"UTC\")\n yest = discover_sessions.parse_time_expr(\"yesterday\", \"UTC\")\n self.assertEqual(yest.end_utc, today.start_utc)\n self.assertEqual(yest.end_utc - yest.start_utc, timedelta(days=1))\n\n def test_bare_iso_date(self):\n tr = discover_sessions.parse_time_expr(\"2026-04-22\", \"UTC\")\n self.assertEqual(tr.end_utc - tr.start_utc, timedelta(days=1))\n self.assertEqual(tr.start_utc.year, 2026)\n self.assertEqual(tr.start_utc.month, 4)\n self.assertEqual(tr.start_utc.day, 22)\n\n def test_iso_date_range_inclusive(self):\n # \"2026-04-22 to 2026-04-25\" → 4 calendar days (22, 23, 24, 25).\n tr = discover_sessions.parse_time_expr(\"2026-04-22 to 2026-04-25\", \"UTC\")\n self.assertEqual(tr.end_utc - tr.start_utc, timedelta(days=4))\n\n def test_explicit_iso_datetime(self):\n tr = discover_sessions.parse_time_expr(\n \"2026-04-22T10:00:00Z\", \"UTC\"\n )\n self.assertEqual(tr.start_utc.year, 2026)\n self.assertIn(\"since\", tr.expr)\n\n def test_unparseable_expression_raises(self):\n with self.assertRaises(SystemExit) as ctx:\n discover_sessions.parse_time_expr(\"nope\", \"UTC\")\n self.assertIn(\"cannot parse\", str(ctx.exception))\n\n def test_unknown_tz_raises(self):\n with self.assertRaises(SystemExit) as ctx:\n discover_sessions.parse_time_expr(\"today\", \"Mars/Olympus\")\n self.assertIn(\"unknown IANA timezone\", str(ctx.exception))\n\n\n# -----------------------------------------------------------------------------\n# compose_sql — pure\n# -----------------------------------------------------------------------------\n\n\ndef _basic_tr() -> discover_sessions.TimeRange:\n return discover_sessions.TimeRange(\n start_utc=datetime(2026, 4, 22, 0, 0, 0, tzinfo=timezone.utc),\n end_utc=datetime(2026, 4, 22, 23, 0, 0, tzinfo=timezone.utc),\n expr=\"test\", tz_name=\"UTC\",\n )\n\n\nclass ComposeSqlTests(unittest.TestCase):\n\n def test_no_filters_skips_joins_and_uses_no_distinct(self):\n sql = discover_sessions.compose_sql(\n tr=_basic_tr(), agent=None, channel=None,\n outcome=None, grep=None, limit=20,\n )\n # Strip SQL comments so the JOIN/DISTINCT mentions in the template\n # docblock don't fool the naive substring check.\n executable = \"\\n\".join(\n line for line in sql.splitlines() if not line.strip().startswith(\"--\")\n )\n self.assertNotIn(\"JOIN \", executable)\n self.assertNotIn(\"DISTINCT\", executable)\n self.assertIn(\"ssot__StartTimestamp__c >= '2026-04-22T00:00:00.000Z'\", executable)\n self.assertIn(\"ssot__StartTimestamp__c \u003c '2026-04-22T23:00:00.000Z'\", executable)\n\n def test_agent_filter_adds_participant_join(self):\n sql = discover_sessions.compose_sql(\n tr=_basic_tr(), agent=\"MyAgent\", channel=None,\n outcome=None, grep=None, limit=20,\n )\n self.assertIn(\"ssot__AiAgentSessionParticipant__dlm\", sql)\n self.assertIn(\"p.ssot__AiAgentApiName__c = 'MyAgent'\", sql)\n self.assertIn(\"DISTINCT\", sql)\n\n def test_messaging_channel_maps_to_scrt2_string(self):\n sql = discover_sessions.compose_sql(\n tr=_basic_tr(), agent=None, channel=\"Messaging\",\n outcome=None, grep=None, limit=20,\n )\n self.assertIn(\"'SCRT2 - EmbeddedMessaging'\", sql)\n\n def test_unknown_channel_passes_through(self):\n sql = discover_sessions.compose_sql(\n tr=_basic_tr(), agent=None, channel=\"Custom_Channel\",\n outcome=None, grep=None, limit=20,\n )\n self.assertIn(\"'Custom_Channel'\", sql)\n\n def test_outcome_filter_added(self):\n sql = discover_sessions.compose_sql(\n tr=_basic_tr(), agent=None, channel=None,\n outcome=\"ESCALATED\", grep=None, limit=20,\n )\n self.assertIn(\"ssot__AiAgentSessionEndType__c = 'ESCALATED'\", sql)\n\n def test_grep_adds_interaction_message_joins(self):\n # Default --grep is case-insensitive (§16d) — both sides are\n # wrapped in LOWER(...). The case-sensitive variant is covered\n # by test_grep_case_sensitive_opt_in below.\n sql = discover_sessions.compose_sql(\n tr=_basic_tr(), agent=None, channel=None,\n outcome=None, grep=\"refund\", limit=20,\n )\n self.assertIn(\"ssot__AiAgentInteraction__dlm\", sql)\n self.assertIn(\"ssot__AiAgentInteractionMessage__dlm\", sql)\n self.assertIn(\"LOWER(m.ssot__ContentText__c) LIKE LOWER('%refund%')\", sql)\n self.assertIn(\"ESCAPE '!'\", sql)\n\n def test_grep_case_sensitive_opt_in(self):\n # --grep-case-sensitive bypasses the LOWER() wrap — restores the\n # exact-match shape for the rare user who wants it.\n sql = discover_sessions.compose_sql(\n tr=_basic_tr(), agent=None, channel=None,\n outcome=None, grep=\"Refund\", grep_case_sensitive=True, limit=20,\n )\n self.assertIn(\"m.ssot__ContentText__c LIKE '%Refund%'\", sql)\n self.assertNotIn(\"LOWER(\", sql)\n self.assertIn(\"ESCAPE '!'\", sql)\n\n def test_grep_escapes_like_wildcards(self):\n sql = discover_sessions.compose_sql(\n tr=_basic_tr(), agent=None, channel=None,\n outcome=None, grep=\"100%_off\", limit=20,\n )\n # Both `%` and `_` get the `!` escape prefix\n self.assertIn(\"100!%!_off\", sql)\n\n def test_grep_escapes_single_quotes(self):\n sql = discover_sessions.compose_sql(\n tr=_basic_tr(), agent=None, channel=None,\n outcome=None, grep=\"O'Brien\", limit=20,\n )\n # Single quote doubled per SQL escaping rule\n self.assertIn(\"O''Brien\", sql)\n\n def test_limit_substituted(self):\n sql = discover_sessions.compose_sql(\n tr=_basic_tr(), agent=None, channel=None,\n outcome=None, grep=None, limit=42,\n )\n self.assertIn(\"42\", sql)\n\n\n# -----------------------------------------------------------------------------\n# fetch_agent_names — mocks dc.post\n# -----------------------------------------------------------------------------\n\n\nclass FetchAgentNamesTests(unittest.TestCase):\n\n def test_empty_session_ids_returns_empty_dict_no_post(self):\n with mock.patch.object(discover_sessions, \"post\") as p:\n out = discover_sessions.fetch_agent_names(\n [], instance_url=\"https://x\", token=\"t\",\n )\n self.assertEqual(out, {})\n p.assert_not_called()\n\n def test_returns_dict_keyed_by_session_id(self):\n with mock.patch.object(discover_sessions, \"post\") as p:\n p.return_value = [\n {\n \"ssot__AiAgentSessionId__c\": \"sid1\",\n \"ssot__AiAgentApiName__c\": \"AgentA\",\n },\n {\n \"ssot__AiAgentSessionId__c\": \"sid2\",\n \"ssot__AiAgentApiName__c\": \"AgentB\",\n },\n ]\n out = discover_sessions.fetch_agent_names(\n [\"sid1\", \"sid2\"], instance_url=\"https://x\", token=\"t\",\n )\n self.assertEqual(out, {\"sid1\": \"AgentA\", \"sid2\": \"AgentB\"})\n\n def test_skips_rows_with_empty_agent_name(self):\n with mock.patch.object(discover_sessions, \"post\") as p:\n p.return_value = [\n {\"ssot__AiAgentSessionId__c\": \"sid1\", \"ssot__AiAgentApiName__c\": \"\"},\n {\"ssot__AiAgentSessionId__c\": \"sid2\", \"ssot__AiAgentApiName__c\": \"AgentB\"},\n ]\n out = discover_sessions.fetch_agent_names(\n [\"sid1\", \"sid2\"], instance_url=\"https://x\", token=\"t\",\n )\n self.assertEqual(out, {\"sid2\": \"AgentB\"})\n\n def test_dcqueryerror_returns_empty_dict_non_fatal(self):\n with mock.patch.object(discover_sessions, \"post\") as p:\n p.side_effect = DCQueryError(\"boom\")\n out = discover_sessions.fetch_agent_names(\n [\"sid1\"], instance_url=\"https://x\", token=\"t\",\n )\n self.assertEqual(out, {})\n\n def test_uses_user_row_name_when_agent_row_is_not_set(self):\n # MyAgent-shape session: AGENT row carries 'NOT_SET',\n # USER row carries the real api_name. Old code filtered to\n # role=AGENT and would return 'NOT_SET' (or nothing). New code\n # is role-agnostic and recovers the api_name from the USER row.\n with mock.patch.object(discover_sessions, \"post\") as p:\n p.return_value = [\n {\"ssot__AiAgentSessionId__c\": \"sid1\", \"ssot__AiAgentApiName__c\": \"NOT_SET\"},\n {\"ssot__AiAgentSessionId__c\": \"sid1\", \"ssot__AiAgentApiName__c\": \"MyAgent\"},\n ]\n out = discover_sessions.fetch_agent_names(\n [\"sid1\"], instance_url=\"https://x\", token=\"t\",\n )\n self.assertEqual(out, {\"sid1\": \"MyAgent\"})\n\n def test_drops_session_when_every_row_is_not_set(self):\n # No usable api_name anywhere — picker should show '—' for the\n # session, which is more honest than displaying the literal\n # 'NOT_SET' string. Implementation contract: missing key in dict.\n with mock.patch.object(discover_sessions, \"post\") as p:\n p.return_value = [\n {\"ssot__AiAgentSessionId__c\": \"sid1\", \"ssot__AiAgentApiName__c\": \"NOT_SET\"},\n {\"ssot__AiAgentSessionId__c\": \"sid1\", \"ssot__AiAgentApiName__c\": \"\"},\n {\"ssot__AiAgentSessionId__c\": \"sid1\", \"ssot__AiAgentApiName__c\": None},\n ]\n out = discover_sessions.fetch_agent_names(\n [\"sid1\"], instance_url=\"https://x\", token=\"t\",\n )\n self.assertNotIn(\"sid1\", out)\n self.assertEqual(out, {})\n\n def test_picks_lexicographic_first_on_handoff_session(self):\n # Multi-agent handoff: two distinct AGENT api_names appear on\n # different rows for the same session. Dominant-agent rule\n # (sorted(...)[0]) matches the policy used by fetch_dc — every\n # writer agrees on the session's namesake.\n with mock.patch.object(discover_sessions, \"post\") as p:\n p.return_value = [\n {\"ssot__AiAgentSessionId__c\": \"sid1\", \"ssot__AiAgentApiName__c\": \"ZAgent\"},\n {\"ssot__AiAgentSessionId__c\": \"sid1\", \"ssot__AiAgentApiName__c\": \"AAgent\"},\n ]\n out = discover_sessions.fetch_agent_names(\n [\"sid1\"], instance_url=\"https://x\", token=\"t\",\n )\n self.assertEqual(out, {\"sid1\": \"AAgent\"})\n\n def test_sql_does_not_filter_by_role(self):\n # Lock the no-role-filter contract — if a future \"tighten this\n # back up\" refactor re-adds the AGENT filter, MyAgent\n # sessions silently regress to '—' or 'NOT_SET' in the picker.\n captured = {}\n\n def fake_post(sql, *_a, **_k):\n captured[\"sql\"] = sql\n return []\n\n with mock.patch.object(discover_sessions, \"post\", side_effect=fake_post):\n discover_sessions.fetch_agent_names(\n [\"sid1\"], instance_url=\"https://x\", token=\"t\",\n )\n self.assertNotIn(\"AiAgentSessionParticipantRole\", captured[\"sql\"])\n self.assertNotIn(\"'AGENT'\", captured[\"sql\"])\n\n\n# -----------------------------------------------------------------------------\n# render_picker — pure\n# -----------------------------------------------------------------------------\n\n\ndef _picker_filters(**overrides) -> dict:\n base = {\"agent\": None, \"channel\": None, \"outcome\": None, \"grep\": None}\n base.update(overrides)\n return base\n\n\nclass RenderPickerTests(unittest.TestCase):\n\n def test_zero_rows_emits_widen_hint_with_composed_sql(self):\n out = discover_sessions.render_picker(\n rows=[], agent_by_sid={}, org=\"my-org\",\n tr=_basic_tr(), filters=_picker_filters(),\n composed_sql=\"SELECT 1\",\n )\n self.assertIn(\"No sessions matched\", out)\n self.assertIn(\"```sql\\nSELECT 1\\n```\", out)\n self.assertIn(\"Try widening\", out)\n\n def test_renders_markdown_table_with_one_row(self):\n rows = [{\n \"ssot__Id__c\": \"019dface-0000-7000-8000-000000000001\",\n \"ssot__StartTimestamp__c\": \"2026-04-22T10:00:00Z\",\n \"ssot__EndTimestamp__c\": \"2026-04-22T10:01:30Z\",\n \"ssot__AiAgentChannelType__c\": \"SCRT2 - EmbeddedMessaging\",\n \"ssot__AiAgentSessionEndType__c\": \"USER_ENDED\",\n }]\n out = discover_sessions.render_picker(\n rows=rows, agent_by_sid={\"019dface-0000-7000-8000-000000000001\": \"AgentA\"},\n org=\"my-org\", tr=_basic_tr(),\n filters=_picker_filters(), composed_sql=\"\",\n )\n self.assertIn(\"Found **1** session\", out)\n self.assertIn(\"`019dface-0000-7000-8000-000000000001`\", out)\n self.assertIn(\"AgentA\", out)\n # Reverse-aliased channel\n self.assertIn(\"Messaging\", out)\n # Duration formatter\n self.assertIn(\"1m 30s\", out)\n self.assertIn(\"USER_ENDED\", out)\n\n def test_handles_session_without_known_agent_with_dash(self):\n rows = [{\n \"ssot__Id__c\": \"sid1\",\n \"ssot__StartTimestamp__c\": \"2026-04-22T10:00:00Z\",\n \"ssot__EndTimestamp__c\": \"2026-04-22T10:00:30Z\",\n \"ssot__AiAgentChannelType__c\": \"Builder\",\n \"ssot__AiAgentSessionEndType__c\": None,\n }]\n out = discover_sessions.render_picker(\n rows=rows, agent_by_sid={}, org=\"my-org\",\n tr=_basic_tr(), filters=_picker_filters(), composed_sql=\"\",\n )\n self.assertIn(\"| — |\", out) # agent + outcome both render as \"—\"\n self.assertIn(\"30s\", out)\n\n def test_filter_bits_appear_in_header(self):\n rows = [{\n \"ssot__Id__c\": \"sid1\",\n \"ssot__StartTimestamp__c\": \"2026-04-22T10:00:00Z\",\n \"ssot__EndTimestamp__c\": \"2026-04-22T10:00:30Z\",\n \"ssot__AiAgentChannelType__c\": \"Builder\",\n \"ssot__AiAgentSessionEndType__c\": \"ESCALATED\",\n }]\n out = discover_sessions.render_picker(\n rows=rows, agent_by_sid={}, org=\"my-org\", tr=_basic_tr(),\n filters=_picker_filters(agent=\"MyAgent\", outcome=\"ESCALATED\"),\n composed_sql=\"\",\n )\n self.assertIn(\"agent=MyAgent\", out)\n self.assertIn(\"outcome=ESCALATED\", out)\n\n\n# -----------------------------------------------------------------------------\n# main — argv-driven exit codes\n# -----------------------------------------------------------------------------\n\n\nclass MainTests(unittest.TestCase):\n\n def _argv(self, *extra: str) -> list[str]:\n # Explicit `--org` so no test shells out for the sf default target org.\n return [\"discover_sessions.py\", \"--org\", \"my-org\", *extra]\n\n def test_main_exit_zero_when_rows_returned(self):\n with mock.patch.object(discover_sessions.sys, \"argv\", self._argv()):\n with mock.patch.object(\n discover_sessions, \"resolve_org\",\n return_value=(\"https://example.salesforce.com\", \"TOKEN\"),\n ):\n with mock.patch.object(\n discover_sessions, \"post\",\n return_value=[{\n \"ssot__Id__c\": \"sid1\",\n \"ssot__StartTimestamp__c\": \"2026-04-22T00:00:00Z\",\n \"ssot__EndTimestamp__c\": \"2026-04-22T00:01:00Z\",\n \"ssot__AiAgentChannelType__c\": \"Builder\",\n \"ssot__AiAgentSessionEndType__c\": \"USER_ENDED\",\n }],\n ):\n rc = discover_sessions.main()\n self.assertEqual(rc, 0)\n\n def test_main_exit_two_when_zero_rows(self):\n with mock.patch.object(discover_sessions.sys, \"argv\", self._argv()):\n with mock.patch.object(\n discover_sessions, \"resolve_org\",\n return_value=(\"https://x\", \"T\"),\n ):\n with mock.patch.object(discover_sessions, \"post\", return_value=[]):\n rc = discover_sessions.main()\n self.assertEqual(rc, 2)\n\n def test_main_exit_one_when_dc_query_fails(self):\n with mock.patch.object(discover_sessions.sys, \"argv\", self._argv()):\n with mock.patch.object(\n discover_sessions, \"resolve_org\",\n return_value=(\"https://x\", \"T\"),\n ):\n with mock.patch.object(\n discover_sessions, \"post\",\n side_effect=DCQueryError(\"HTTP 500\"),\n ):\n rc = discover_sessions.main()\n self.assertEqual(rc, 1)\n\n def test_main_rejects_zero_or_negative_limit(self):\n with mock.patch.object(\n discover_sessions.sys, \"argv\",\n self._argv(\"--limit\", \"0\"),\n ):\n with self.assertRaises(SystemExit) as ctx:\n discover_sessions.main()\n self.assertIn(\"--limit must be >= 1\", str(ctx.exception))\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":19469,"content_sha256":"f1c2c7343f08b3144740b2e7191f0f40bf9760cfaa78973ce32902cb24da1f56"},{"filename":"scripts/tests/test_fetch_dc_helpers.py","content":"\"\"\"Tests for ``fetch_dc`` helpers + DC-access-denied flow.\n\nComplements ``test_fetch_dc_identity.py`` (which targets ``_resolve_identity``\nspecifically). This file lifts coverage of the other public helpers:\n\n- ``DcAccessDenied`` exception carrier\n- ``preflight_dc_access`` 401/403 classification + happy path\n- ``_emit_dc_access_denied_preamble`` headless JSON shape\n- ``_handle_dc_access_denied`` interactive (tty) vs headless branches\n- ``_in_list`` SQL IN-fragment helper (dedup + NOT_SET filter)\n- ``_extract_trace_ids`` runtime trace-id extraction (DMO + HTML-escaped)\n- ``_preflight_templates`` SQL template existence check\n\nAll subprocess + DC-access boundaries are mocked.\n\"\"\"\nfrom __future__ import annotations\n\nimport io\nimport json\nimport unittest\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport fetch_dc # type: ignore\nfrom dc import DCQueryError # type: ignore\n\n\n# -----------------------------------------------------------------------------\n# DcAccessDenied — small carrier\n# -----------------------------------------------------------------------------\n\n\nclass DcAccessDeniedTests(unittest.TestCase):\n\n def test_str_includes_reason_and_detail(self):\n e = fetch_dc.DcAccessDenied(\"401\", \"Unauthorized\")\n self.assertEqual(e.reason, \"401\")\n self.assertEqual(e.detail, \"Unauthorized\")\n self.assertIn(\"401: Unauthorized\", str(e))\n\n\n# -----------------------------------------------------------------------------\n# preflight_dc_access — classify HTTP error code\n# -----------------------------------------------------------------------------\n\n\nclass PreflightDcAccessTests(unittest.TestCase):\n\n def test_returns_url_and_token_on_success(self):\n with mock.patch.object(\n fetch_dc, \"resolve_org\",\n return_value=(\"https://example.salesforce.com\", \"TOKEN\"),\n ):\n with mock.patch.object(fetch_dc, \"post\", return_value=[]):\n url, token = fetch_dc.preflight_dc_access(\"sid\", \"my-org\")\n self.assertEqual(url, \"https://example.salesforce.com\")\n self.assertEqual(token, \"TOKEN\")\n\n def test_raises_dc_access_denied_on_401(self):\n with mock.patch.object(\n fetch_dc, \"resolve_org\",\n return_value=(\"https://x\", \"T\"),\n ):\n with mock.patch.object(\n fetch_dc, \"post\",\n side_effect=DCQueryError(\"http=401 Unauthorized\"),\n ):\n with self.assertRaises(fetch_dc.DcAccessDenied) as ctx:\n fetch_dc.preflight_dc_access(\"sid\", \"my-org\")\n self.assertEqual(ctx.exception.reason, \"401\")\n\n def test_raises_dc_access_denied_on_403(self):\n with mock.patch.object(\n fetch_dc, \"resolve_org\",\n return_value=(\"https://x\", \"T\"),\n ):\n with mock.patch.object(\n fetch_dc, \"post\",\n side_effect=DCQueryError(\"http=403 Forbidden\"),\n ):\n with self.assertRaises(fetch_dc.DcAccessDenied) as ctx:\n fetch_dc.preflight_dc_access(\"sid\", \"my-org\")\n self.assertEqual(ctx.exception.reason, \"403\")\n\n def test_raises_dc_access_denied_on_other_dc_error(self):\n with mock.patch.object(\n fetch_dc, \"resolve_org\",\n return_value=(\"https://x\", \"T\"),\n ):\n with mock.patch.object(\n fetch_dc, \"post\",\n side_effect=DCQueryError(\"http=500 ISE\"),\n ):\n with self.assertRaises(fetch_dc.DcAccessDenied) as ctx:\n fetch_dc.preflight_dc_access(\"sid\", \"my-org\")\n # 5xx falls into the catch-all dc_probe_failed bucket.\n self.assertEqual(ctx.exception.reason, \"dc_probe_failed\")\n\n\n# -----------------------------------------------------------------------------\n# _emit_dc_access_denied_preamble — JSON shape\n# -----------------------------------------------------------------------------\n\n\nclass EmitPreambleTests(unittest.TestCase):\n\n def test_emits_single_line_json_with_two_options(self):\n buf = io.StringIO()\n with mock.patch(\"sys.stdout\", buf):\n fetch_dc._emit_dc_access_denied_preamble(\"401\", \"Unauthorized\")\n printed = buf.getvalue().strip()\n # One single-line JSON record\n self.assertEqual(printed.count(\"\\n\"), 0)\n payload = json.loads(printed)\n self.assertEqual(payload[\"status\"], \"DC_ACCESS_DENIED\")\n self.assertEqual(payload[\"reason\"], \"401\")\n self.assertEqual(payload[\"detail\"], \"Unauthorized\")\n # Standalone d360 has retry + cancel only — no sibling-skill switch.\n self.assertEqual(len(payload[\"options\"]), 2)\n codes = [o[\"code\"] for o in payload[\"options\"]]\n self.assertEqual(codes, [\"1\", \"2\"])\n actions = [o[\"action\"] for o in payload[\"options\"]]\n self.assertEqual(actions, [\"retry\", \"cancel\"])\n\n\n# -----------------------------------------------------------------------------\n# _handle_dc_access_denied — branches on tty vs not, + interactive choices\n# -----------------------------------------------------------------------------\n\n\nclass HandleDcAccessDeniedTests(unittest.TestCase):\n\n def _exc(self) -> fetch_dc.DcAccessDenied:\n return fetch_dc.DcAccessDenied(\"401\", \"Unauthorized\")\n\n def test_headless_emits_preamble_and_returns_exit_code(self):\n with mock.patch.object(\n fetch_dc, \"_emit_dc_access_denied_preamble\"\n ) as emit:\n rc = fetch_dc._handle_dc_access_denied(\n self._exc(), session_id=\"sid\", is_tty=False,\n )\n emit.assert_called_once_with(\"401\", \"Unauthorized\")\n self.assertEqual(rc, fetch_dc.EXIT_DC_ACCESS_DENIED)\n\n def test_interactive_choice_1_returns_exit_code(self):\n # User picks \"1\" → caller signaled to retry.\n with mock.patch.object(fetch_dc.sys, \"stdin\", io.StringIO(\"1\\n\")):\n with mock.patch.object(fetch_dc, \"_log\"):\n rc = fetch_dc._handle_dc_access_denied(\n self._exc(), session_id=\"sid\", is_tty=True,\n )\n self.assertEqual(rc, fetch_dc.EXIT_DC_ACCESS_DENIED)\n\n def test_interactive_choice_2_returns_zero_cancel(self):\n # Standalone d360: choice \"2\" is cancel (no sibling-skill switch).\n with mock.patch.object(fetch_dc.sys, \"stdin\", io.StringIO(\"2\\n\")):\n with mock.patch.object(fetch_dc, \"_log\"):\n rc = fetch_dc._handle_dc_access_denied(\n self._exc(), session_id=\"sid\", is_tty=True,\n )\n self.assertEqual(rc, 0)\n\n def test_interactive_unknown_choice_treated_as_cancel(self):\n # \"\" / \"9\" / random → cancel.\n with mock.patch.object(fetch_dc.sys, \"stdin\", io.StringIO(\"\\n\")):\n with mock.patch.object(fetch_dc, \"_log\"):\n rc = fetch_dc._handle_dc_access_denied(\n self._exc(), session_id=\"sid\", is_tty=True,\n )\n self.assertEqual(rc, 0)\n\n def test_interactive_keyboard_interrupt_returns_zero(self):\n # stdin.readline() raising KeyboardInterrupt → graceful 0.\n fake_stdin = mock.MagicMock()\n fake_stdin.readline.side_effect = KeyboardInterrupt\n with mock.patch.object(fetch_dc.sys, \"stdin\", fake_stdin):\n with mock.patch.object(fetch_dc, \"_log\"):\n rc = fetch_dc._handle_dc_access_denied(\n self._exc(), session_id=\"sid\", is_tty=True,\n )\n self.assertEqual(rc, 0)\n\n\n# -----------------------------------------------------------------------------\n# _in_list — SQL IN-clause fragment builder\n# -----------------------------------------------------------------------------\n\n\nclass InListTests(unittest.TestCase):\n\n def test_renders_quoted_csv_inside_parens(self):\n self.assertEqual(fetch_dc._in_list([\"a\", \"b\", \"c\"]), \"('a','b','c')\")\n\n def test_dedups_and_preserves_first_occurrence_order(self):\n self.assertEqual(\n fetch_dc._in_list([\"a\", \"b\", \"a\", \"c\", \"b\"]), \"('a','b','c')\"\n )\n\n def test_drops_empty_string(self):\n self.assertEqual(fetch_dc._in_list([\"a\", \"\", \"b\"]), \"('a','b')\")\n\n def test_drops_NOT_SET_token(self):\n self.assertEqual(\n fetch_dc._in_list([\"a\", \"NOT_SET\", \"b\"]), \"('a','b')\"\n )\n\n def test_empty_input_returns_empty_parens(self):\n # All inputs filtered → \"()\". SQL won't accept this but that's the\n # caller's concern — the helper itself is mechanical.\n self.assertEqual(fetch_dc._in_list([]), \"()\")\n self.assertEqual(fetch_dc._in_list([\"\", \"NOT_SET\"]), \"()\")\n\n\n# -----------------------------------------------------------------------------\n# _extract_trace_ids — DMO field + HTML-escaped fallback\n# -----------------------------------------------------------------------------\n\n\nclass ExtractTraceIdsTests(unittest.TestCase):\n\n def test_uses_telemetry_trace_id_when_populated(self):\n rows = [{\"ssot__TelemetryTraceId__c\": \"abc123\"}]\n self.assertEqual(fetch_dc._extract_trace_ids(rows), [\"abc123\"])\n\n def test_falls_back_to_attribute_text_internalTraceId(self):\n # Simulate the HTML-escaped JSON-in-a-string shape DC stores.\n rows = [{\n \"ssot__TelemetryTraceId__c\": \"\",\n \"ssot__AttributeText__c\": '"internalTraceId":"deadbeef"',\n }]\n self.assertEqual(fetch_dc._extract_trace_ids(rows), [\"deadbeef\"])\n\n def test_dedupes_preserving_first_occurrence(self):\n rows = [\n {\"ssot__TelemetryTraceId__c\": \"x\"},\n {\"ssot__TelemetryTraceId__c\": \"y\"},\n {\"ssot__TelemetryTraceId__c\": \"x\"},\n ]\n self.assertEqual(fetch_dc._extract_trace_ids(rows), [\"x\", \"y\"])\n\n def test_drops_NOT_SET_token(self):\n rows = [{\"ssot__TelemetryTraceId__c\": \"NOT_SET\"}]\n self.assertEqual(fetch_dc._extract_trace_ids(rows), [])\n\n def test_returns_empty_when_no_trace_id_anywhere(self):\n rows = [{\"ssot__TelemetryTraceId__c\": \"\", \"ssot__AttributeText__c\": \"\"}]\n self.assertEqual(fetch_dc._extract_trace_ids(rows), [])\n\n\n# -----------------------------------------------------------------------------\n# _preflight_templates — SQL templates exist on disk\n# -----------------------------------------------------------------------------\n\n\nclass PreflightTemplatesTests(unittest.TestCase):\n\n def test_succeeds_when_all_templates_present(self):\n # All the production .sql files ship in assets/dc/. This call should\n # complete cleanly under the in-tree install.\n fetch_dc._preflight_templates() # raises if any missing\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10857,"content_sha256":"865cb157b6ba2ba38c4c11e9f320a4155f88cd2be650f54a21e7e27b4460cb65"},{"filename":"scripts/tests/test_fetch_dc_identity.py","content":"\"\"\"Tests for ``fetch_dc._resolve_identity`` + ``sf org display`` fallback.\n\nCloses a real-world failure mode: ``sessions[0].ssot__InternalOrganizationId__c``\nis occasionally null in SSOT materialization despite every other field\nbeing populated (e.g. session ``019dface-0000-7000-8000-000000000001`` in\n``my-org-alias``). The authenticated ``sf`` alias knows the org id\nregardless, so we fall back to ``sf org display --target-org \u003calias> --json``\nand parse ``.result.id``. If that also fails, a unified diagnostic\nsurfaces both failure modes.\n\nEvery test patches ``subprocess.run`` at the ``fetch_dc`` module level so\nno real CLI is ever invoked.\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport subprocess\nimport unittest\nfrom types import SimpleNamespace\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport fetch_dc # type: ignore\n\n\n_ORG_ALIAS = \"my-org-alias\"\n_FULL_ORG_ID_18 = \"00D0000000000000EA\"\n_EXPECTED_ORG_ID_15 = \"00D000000000000\"\n_AGENT_API = \"DemoAgent\"\n_AGENT_VER = \"v5\"\n\n\ndef _agent_participant() -> dict:\n return {\n \"ssot__AiAgentSessionParticipantRole__c\": \"AGENT\",\n \"ssot__AiAgentApiName__c\": _AGENT_API,\n \"ssot__AiAgentVersionApiName__c\": _AGENT_VER,\n }\n\n\ndef _sf_cli_success_stdout(org_id: str = _FULL_ORG_ID_18) -> str:\n \"\"\"Shape mirrors the real ``sf org display --json`` payload.\"\"\"\n return json.dumps({\n \"status\": 0,\n \"result\": {\n \"id\": org_id,\n \"instanceUrl\": \"https://example.my.salesforce.com\",\n \"accessToken\": \"REDACTED\",\n },\n })\n\n\nclass ResolveIdentityDmoPopulatedTests(unittest.TestCase):\n \"\"\"Happy path — DMO field carries a valid 18-char id, no subprocess spawn.\"\"\"\n\n def test_resolve_identity_uses_dmo_when_populated(self):\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n participants = [_agent_participant()]\n\n with mock.patch.object(fetch_dc.subprocess, \"run\") as m_run:\n org_id_15, agent, ver = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n\n self.assertEqual(org_id_15, _EXPECTED_ORG_ID_15)\n self.assertEqual(agent, _AGENT_API)\n self.assertEqual(ver, _AGENT_VER)\n m_run.assert_not_called()\n\n\nclass ResolveIdentityDmoFallbackTests(unittest.TestCase):\n \"\"\"DMO field null/empty/short — fall back to ``sf org display``.\"\"\"\n\n def _patch_sf_cli_ok(self, org_id: str = _FULL_ORG_ID_18):\n return mock.patch.object(\n fetch_dc.subprocess, \"run\",\n return_value=SimpleNamespace(\n stdout=_sf_cli_success_stdout(org_id),\n stderr=\"\",\n returncode=0,\n ),\n )\n\n def test_resolve_identity_falls_back_to_sf_cli_when_dmo_null(self):\n sessions = [{\"ssot__InternalOrganizationId__c\": None}]\n participants = [_agent_participant()]\n\n with self._patch_sf_cli_ok() as m_run:\n org_id_15, agent, ver = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n\n self.assertEqual(org_id_15, _EXPECTED_ORG_ID_15)\n self.assertEqual(agent, _AGENT_API)\n self.assertEqual(ver, _AGENT_VER)\n # Exact argv shape matters — it's the public contract with the sf CLI.\n args, kwargs = m_run.call_args\n self.assertEqual(\n args[0],\n [\"sf\", \"org\", \"display\", \"--target-org\", _ORG_ALIAS, \"--json\"],\n )\n self.assertTrue(kwargs.get(\"check\"))\n self.assertTrue(kwargs.get(\"capture_output\"))\n self.assertTrue(kwargs.get(\"text\"))\n\n def test_resolve_identity_falls_back_when_dmo_empty_string(self):\n sessions = [{\"ssot__InternalOrganizationId__c\": \"\"}]\n participants = [_agent_participant()]\n\n with self._patch_sf_cli_ok():\n org_id_15, _, _ = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n self.assertEqual(org_id_15, _EXPECTED_ORG_ID_15)\n\n def test_resolve_identity_falls_back_when_dmo_shorter_than_15(self):\n sessions = [{\"ssot__InternalOrganizationId__c\": \"shortid\"}]\n participants = [_agent_participant()]\n\n with self._patch_sf_cli_ok():\n org_id_15, _, _ = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n self.assertEqual(org_id_15, _EXPECTED_ORG_ID_15)\n\n\nclass ResolveIdentityBothSourcesFailTests(unittest.TestCase):\n \"\"\"When both the DMO field AND the CLI fallback fail, surface both.\"\"\"\n\n def test_resolve_identity_raises_when_both_sources_fail(self):\n sessions = [{\"ssot__InternalOrganizationId__c\": None}]\n participants = [_agent_participant()]\n\n err = subprocess.CalledProcessError(\n returncode=1,\n cmd=[\"sf\", \"org\", \"display\", \"--target-org\", _ORG_ALIAS, \"--json\"],\n stderr=\"No AuthInfo found for name \" + _ORG_ALIAS,\n )\n with mock.patch.object(fetch_dc.subprocess, \"run\", side_effect=err):\n with self.assertRaises(SystemExit) as ctx:\n fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n\n msg = str(ctx.exception)\n # Must mention both failure modes so users know which to fix.\n self.assertIn(\"both DMO field and sf org display failed\", msg)\n self.assertIn(_ORG_ALIAS, msg)\n self.assertIn(\"CalledProcessError\", msg)\n\n\nclass ResolveIdentityNoAgentParticipantsTests(unittest.TestCase):\n \"\"\"Recently-created sessions can land here while STDM is still\n materializing — the AGENT participant rows haven't been written yet.\n The error must hint at retry rather than implying a malformed session.\n \"\"\"\n\n def test_resolve_identity_raises_with_retry_hint_when_no_agent_participants(self):\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n # No AGENT-role participants; only a USER row.\n participants = [{\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"\",\n \"ssot__AiAgentVersionApiName__c\": \"\",\n }]\n\n with self.assertRaises(SystemExit) as ctx:\n fetch_dc._resolve_identity(sessions, participants, _ORG_ALIAS)\n\n msg = str(ctx.exception)\n # Identifying string for the error class.\n self.assertIn(\"no AGENT participants\", msg)\n # New (Fix #6) wording — leads with retry, then falls back to malformed.\n self.assertIn(\"retry in a few minutes\", msg)\n self.assertIn(\"STDM materialization\", msg)\n\n def test_resolve_identity_raises_when_only_empty_field_participants(self):\n # AGENT-role row exists but agent_api_name + agent_version are blank.\n # Same outcome — no usable identity, same retry-hint error.\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n participants = [{\n \"ssot__AiAgentSessionParticipantRole__c\": \"AGENT\",\n \"ssot__AiAgentApiName__c\": \"\",\n \"ssot__AiAgentVersionApiName__c\": \"\",\n }]\n\n with self.assertRaises(SystemExit) as ctx:\n fetch_dc._resolve_identity(sessions, participants, _ORG_ALIAS)\n\n self.assertIn(\"no AGENT participants\", str(ctx.exception))\n\n\nclass ResolveIdentityUserRowFallbackTests(unittest.TestCase):\n \"\"\"Some agent shapes (e.g. the OOTB ``MyAgent`` template) leave\n AGENT-row ``ssot__AiAgentApiName__c`` / ``ssot__AiAgentVersionApiName__c``\n as ``NOT_SET`` indefinitely, while USER-role rows correctly carry the\n api_name. The downstream session is otherwise complete (interactions,\n steps, generations all materialize), so refusing to run wastes signal.\n\n The fallback promotes the api_name from any participant row, stamps\n version=``v0`` (placeholder satisfying ``^v[0-9]+ agentforce-d360-analyze — Skillopedia ` per fs_guard), and\n proceeds. Tests guard against regression — a future \"tighten this back\n up\" refactor would re-break the MyAgent case silently.\n \"\"\"\n\n def test_falls_back_to_user_row_api_name_when_agent_rows_not_set(self):\n # Mirrors a real session shape observed in production:\n # USER rows carry the api_name, AGENT rows are 'NOT_SET'.\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n participants = [\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"MyAgent\",\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n },\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"AGENT\",\n \"ssot__AiAgentApiName__c\": \"NOT_SET\",\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n },\n ]\n\n org_id_15, agent, ver = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n\n self.assertEqual(org_id_15, _EXPECTED_ORG_ID_15)\n self.assertEqual(agent, \"MyAgent\")\n # Placeholder must satisfy the AGENT_VERSION_RE used by paths.session_dir.\n self.assertEqual(ver, \"v0\")\n\n def test_fallback_handles_blank_api_name_on_agent_row(self):\n # AGENT row api_name is empty string (rather than the literal\n # 'NOT_SET'). Both shapes mean the same thing per _NOT_SET.\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n participants = [\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"MyAgent\",\n \"ssot__AiAgentVersionApiName__c\": \"\",\n },\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"AGENT\",\n \"ssot__AiAgentApiName__c\": \"\",\n \"ssot__AiAgentVersionApiName__c\": \"\",\n },\n ]\n\n _, agent, ver = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n self.assertEqual(agent, \"MyAgent\")\n self.assertEqual(ver, \"v0\")\n\n def test_fallback_picks_lexicographic_first_when_multiple_user_names(self):\n # Multi-agent handoff session where USER rows mention both agents.\n # The fallback uses sorted({...})[0] to match the dominant-agent\n # policy used elsewhere in the pipeline.\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n participants = [\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"ZAgent\",\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n },\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"AAgent\",\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n },\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"AGENT\",\n \"ssot__AiAgentApiName__c\": \"NOT_SET\",\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n },\n ]\n\n _, agent, ver = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n self.assertEqual(agent, \"AAgent\")\n self.assertEqual(ver, \"v0\")\n\n def test_agent_row_with_only_version_not_set_still_uses_strict_path(self):\n # Defensive: AGENT row has a real api_name but version=NOT_SET. The\n # strict candidate filter rejects it, but the fallback should still\n # promote the api_name (avoiding a needless raise just because version\n # was missing on the AGENT row).\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n participants = [{\n \"ssot__AiAgentSessionParticipantRole__c\": \"AGENT\",\n \"ssot__AiAgentApiName__c\": \"RealAgent\",\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n }]\n\n _, agent, ver = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n self.assertEqual(agent, \"RealAgent\")\n self.assertEqual(ver, \"v0\")\n\n def test_fallback_rejects_api_name_with_invalid_shape(self):\n # Even though a non-empty api_name is present on a USER row, if it\n # doesn't satisfy ^[A-Za-z0-9_]+$ (fs_guard.API_NAME_RE) the fallback\n # MUST fall through to the original SystemExit. Without this guard,\n # paths.session_dir would later reject the dir with an opaque\n # ValidationError far from where the bad value originated.\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n for bad_name in (\"Service Agent\", \"agent-v1\", \"agent.v1\", \"héllo\"):\n with self.subTest(bad_name=bad_name):\n participants = [{\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": bad_name,\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n }]\n with self.assertRaises(SystemExit) as ctx:\n fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n # Falls through to the strict-path SystemExit, NOT a path-\n # validation traceback from fs_guard.\n self.assertIn(\"no AGENT participants\", str(ctx.exception))\n\n def test_fallback_skips_invalid_picks_valid_when_both_present(self):\n # If multiple api_names are seen, the regex filter should drop the\n # invalid ones and the fallback should still succeed with a valid one.\n # Defense against a real session that mixes a malformed display-name\n # row (e.g. legacy data) with a clean USER row.\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n participants = [\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"Service Agent\", # invalid\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n },\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"MyAgent\", # valid\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n },\n ]\n _, agent, ver = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n self.assertEqual(agent, \"MyAgent\")\n self.assertEqual(ver, \"v0\")\n\n def test_fallback_returned_version_matches_agent_version_regex(self):\n # Belt-and-braces: the placeholder version we return MUST match\n # the ^v[0-9]+$ shape enforced by paths.session_dir / fs_guard\n # (see scripts/_shared/paths.py:_validate_agent_triple). If a\n # future change picks a different placeholder ('NOT_SET',\n # 'unknown', 'v0.0', etc.) directory creation downstream will\n # blow up. Test the exact contract here without taking a\n # cross-skill import dependency.\n import re\n\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n participants = [{\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"MyAgent\",\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n }]\n _, _, ver = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n self.assertRegex(ver, r\"^v[0-9]+$\")\n\n\nclass ResolveIdentityCrossRoleVersionHarvestTests(unittest.TestCase):\n \"\"\"Real-data shape: AGENT participant rows had api_name + version both\n NOT_SET, but the USER rows carried the real (api_name, version) pair.\n The prior fallback only harvested api_name from non-AGENT rows and\n stamped version=``v0``, so the dir landed at ``\u003cagent>__v0/`` while\n ``assemble_dc`` reconciled the real version into the tree from\n ``session.identity`` — dir + tree out of sync.\n\n The cross-role harvest closes that gap: when the strict AGENT-only\n candidate list is empty, scan ALL participant rows for a valid\n (api_name, version) pair before falling back to the v0 placeholder.\n Real version wins; placeholder is genuinely a last resort.\n \"\"\"\n\n def test_user_row_carries_real_version_dir_lands_correctly(self):\n # The exact shape found on a live session. AGENT rows: NOT_SET on\n # both fields. USER rows: real pair.\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n participants = [\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"MyAgent\",\n \"ssot__AiAgentVersionApiName__c\": \"v24\",\n },\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"MyAgent\",\n \"ssot__AiAgentVersionApiName__c\": \"v24\",\n },\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"AGENT\",\n \"ssot__AiAgentApiName__c\": \"NOT_SET\",\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n },\n ]\n\n _, agent, ver = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n self.assertEqual(agent, \"MyAgent\")\n # MUST be the real version, NOT the v0 placeholder — that's the\n # whole point of the cross-role harvest.\n self.assertEqual(ver, \"v24\")\n\n def test_falls_through_to_v0_placeholder_when_no_role_carries_version(self):\n # Builder Previewer shape: api_name lands on a USER row but no\n # participant row carries a real version. Must drop through to\n # the v0 placeholder branch (which keeps the pipeline runnable\n # for that legitimate session shape).\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n participants = [\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"MyAgent\",\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n },\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"AGENT\",\n \"ssot__AiAgentApiName__c\": \"NOT_SET\",\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n },\n ]\n\n _, agent, ver = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n self.assertEqual(agent, \"MyAgent\")\n self.assertEqual(ver, \"v0\")\n\n def test_cross_role_rejects_malformed_version(self):\n # Someone wrote a malformed version into a USER row (e.g.\n # 'v1.0', '1', '24'). Must NOT adopt it — fs_guard will reject\n # the dir later. Fall through to v0 placeholder so the pipeline\n # still runs and the assemble_dc reconcile can promote later\n # if a real version shows up elsewhere in the tree.\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n for bad_version in (\"v1.0\", \"1\", \"24\", \"version-24\", \"\"):\n with self.subTest(bad_version=bad_version):\n participants = [\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"MyAgent\",\n \"ssot__AiAgentVersionApiName__c\": bad_version,\n },\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"AGENT\",\n \"ssot__AiAgentApiName__c\": \"NOT_SET\",\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n },\n ]\n _, agent, ver = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n self.assertEqual(agent, \"MyAgent\")\n # Bad version rejected; placeholder kicks in.\n self.assertEqual(ver, \"v0\")\n\n def test_strict_agent_row_precedence_beats_user_row(self):\n # When BOTH AGENT and USER rows carry valid (api_name, version)\n # pairs, the strict-AGENT path runs first and the cross-role\n # branch is unreachable. Pin this precedence so a future\n # refactor can't accidentally reorder the resolution. If the\n # AGENT row's pair were ever shadowed by a USER row's pair, the\n # dominant-agent invariant on handoff sessions breaks.\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n participants = [\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"AAgent\", # lex-first across both rows\n \"ssot__AiAgentVersionApiName__c\": \"v1\",\n },\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"AGENT\",\n \"ssot__AiAgentApiName__c\": \"ZAgent\", # lex-LAST overall\n \"ssot__AiAgentVersionApiName__c\": \"v9\",\n },\n ]\n\n _, agent, ver = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n # AGENT-row precedence: ZAgent/v9 wins, NOT AAgent/v1.\n self.assertEqual(agent, \"ZAgent\")\n self.assertEqual(ver, \"v9\")\n\n def test_cross_role_picks_lex_first_when_multiple_valid_pairs(self):\n # Handoff with two valid (api_name, version) pairs across USER\n # rows. The strict-path dominant-agent rule applies: lex-first\n # wins. This keeps the cross-role harvest aligned with the\n # AGENT-only path's selection policy.\n sessions = [{\"ssot__InternalOrganizationId__c\": _FULL_ORG_ID_18}]\n participants = [\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"ZAgent\",\n \"ssot__AiAgentVersionApiName__c\": \"v9\",\n },\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"USER\",\n \"ssot__AiAgentApiName__c\": \"AAgent\",\n \"ssot__AiAgentVersionApiName__c\": \"v3\",\n },\n {\n \"ssot__AiAgentSessionParticipantRole__c\": \"AGENT\",\n \"ssot__AiAgentApiName__c\": \"NOT_SET\",\n \"ssot__AiAgentVersionApiName__c\": \"NOT_SET\",\n },\n ]\n\n _, agent, ver = fetch_dc._resolve_identity(\n sessions, participants, _ORG_ALIAS,\n )\n self.assertEqual(agent, \"AAgent\")\n self.assertEqual(ver, \"v3\")\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":22543,"content_sha256":"17f0fe535392acaac083f27ea6a45f204602b823a44c60f4654abba255810965"},{"filename":"scripts/tests/test_fetch_dc_main.py","content":"\"\"\"Tests for ``fetch_dc.main`` orchestration.\n\nDrives the CLI via ``argv`` patching. Mocks at three boundaries so we\nexercise the orchestration without spawning real DC/HTTP/sf-CLI work:\n\n- ``preflight_dc_access`` returns (url, token) or raises DcAccessDenied\n- ``_run_waterfall`` stubbed; mutates ctx the way the real waterfall would\n- ``assemble_dc.main_for_session`` patched to no-op\n- ``render_dc.main_for_session`` patched to no-op\n\"\"\"\nfrom __future__ import annotations\n\nimport io\nimport json\nimport unittest\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport fetch_dc # type: ignore\nfrom config import paths # type: ignore\nfrom .fixtures.synthetic_session import IDS, write_to_disk # type: ignore\n\n\ndef _fake_waterfall(ctx: dict) -> None:\n \"\"\"Mutate ctx the way the real waterfall would: stamp identity +\n append a few query entries. Keeps `_write_manifest` happy.\"\"\"\n ctx[\"org_id_15\"] = IDS.ORG_ID_15\n ctx[\"agent_api_name\"] = IDS.AGENT_API\n ctx[\"agent_version\"] = IDS.AGENT_VERSION\n ctx[\"session_shape\"] = \"complete\"\n ctx[\"queries\"].append({\n \"name\": \"sessions\", \"wave\": 1, \"rows\": 1,\n \"elapsed_ms\": 50, \"status\": \"ok\",\n })\n ctx[\"queries\"].append({\n \"name\": \"interactions\", \"wave\": 1, \"rows\": 3,\n \"elapsed_ms\": 60, \"status\": \"ok\",\n })\n\n\nclass _MainHarness:\n \"\"\"Patch DATA_ROOT + the 4 boundary functions so main() runs offline.\"\"\"\n\n def __init__(self, *, raise_dc_access_denied=None,\n is_tty: bool = False):\n self.raise_dc_access_denied = raise_dc_access_denied\n self.is_tty = is_tty\n\n def __enter__(self):\n self._tmp = TemporaryDirectory()\n self._tmpdir = Path(self._tmp.name)\n # Pre-create the session dir so _write_manifest's mkdir works\n # without surprises.\n write_to_disk(self._tmpdir)\n if self.raise_dc_access_denied is not None:\n preflight_mock = mock.patch.object(\n fetch_dc, \"preflight_dc_access\",\n side_effect=self.raise_dc_access_denied,\n )\n else:\n preflight_mock = mock.patch.object(\n fetch_dc, \"preflight_dc_access\",\n return_value=(\"https://example.salesforce.com\", \"TOKEN\"),\n )\n self._patches = [\n mock.patch.object(paths, \"DATA_ROOT\", self._tmpdir),\n preflight_mock,\n mock.patch.object(fetch_dc, \"_run_waterfall\",\n side_effect=_fake_waterfall),\n mock.patch(\"assemble_dc.main_for_session\", return_value=0),\n mock.patch(\"render_dc.main_for_session\", return_value=0),\n mock.patch.object(fetch_dc.sys.stdin, \"isatty\",\n return_value=self.is_tty),\n ]\n for p in self._patches:\n p.start()\n return self\n\n def __exit__(self, *exc):\n for p in self._patches:\n p.stop()\n self._tmp.cleanup()\n\n\n# -----------------------------------------------------------------------------\n# Happy path\n# -----------------------------------------------------------------------------\n\n\nclass MainHappyPathTests(unittest.TestCase):\n\n def test_main_returns_zero_with_no_assemble_no_render(self):\n with _MainHarness():\n with mock.patch.object(\n fetch_dc.sys, \"argv\",\n [\"fetch_dc.py\", \"--session\", IDS.SID, \"--org\", \"my-org\",\n \"--no-assemble\", \"--no-render\"],\n ):\n rc = fetch_dc.main()\n self.assertEqual(rc, 0)\n\n def test_main_writes_manifest_under_data_root(self):\n with _MainHarness() as h:\n with mock.patch.object(\n fetch_dc.sys, \"argv\",\n [\"fetch_dc.py\", \"--session\", IDS.SID, \"--org\", \"my-org\",\n \"--no-assemble\", \"--no-render\"],\n ):\n fetch_dc.main()\n manifest_path = (\n h._tmpdir\n / IDS.ORG_ID_15\n / f\"{IDS.AGENT_API}__{IDS.AGENT_VERSION}\"\n / IDS.SID\n / \"dc._session_manifest.json\"\n )\n self.assertTrue(manifest_path.is_file())\n manifest = json.loads(manifest_path.read_text())\n self.assertEqual(manifest[\"session_id\"], IDS.SID)\n self.assertEqual(manifest[\"session_shape\"], \"complete\")\n\n def test_main_invokes_assemble_and_render_by_default(self):\n # Without --no-assemble / --no-render, both should be called once.\n with _MainHarness():\n with mock.patch.object(\n fetch_dc.sys, \"argv\",\n [\"fetch_dc.py\", \"--session\", IDS.SID, \"--org\", \"my-org\"],\n ):\n # Re-patch the targets at call site to capture.\n with mock.patch(\"assemble_dc.main_for_session\") as ams:\n with mock.patch(\"render_dc.main_for_session\") as rms:\n rc = fetch_dc.main()\n self.assertEqual(rc, 0)\n ams.assert_called_once_with(IDS.SID)\n rms.assert_called_once_with(IDS.SID)\n\n def test_main_skips_assemble_when_flag_set(self):\n with _MainHarness():\n with mock.patch.object(\n fetch_dc.sys, \"argv\",\n [\"fetch_dc.py\", \"--session\", IDS.SID, \"--org\", \"my-org\",\n \"--no-assemble\"],\n ):\n with mock.patch(\"assemble_dc.main_for_session\") as ams:\n with mock.patch(\"render_dc.main_for_session\") as rms:\n rc = fetch_dc.main()\n self.assertEqual(rc, 0)\n ams.assert_not_called()\n rms.assert_called_once()\n\n def test_main_skips_render_when_flag_set(self):\n with _MainHarness():\n with mock.patch.object(\n fetch_dc.sys, \"argv\",\n [\"fetch_dc.py\", \"--session\", IDS.SID, \"--org\", \"my-org\",\n \"--no-render\"],\n ):\n with mock.patch(\"assemble_dc.main_for_session\") as ams:\n with mock.patch(\"render_dc.main_for_session\") as rms:\n rc = fetch_dc.main()\n self.assertEqual(rc, 0)\n ams.assert_called_once()\n rms.assert_not_called()\n\n def test_main_verbose_flag_propagates_to_ctx(self):\n captured_ctx = {}\n\n def capture(ctx):\n captured_ctx.update(ctx)\n _fake_waterfall(ctx)\n\n with _MainHarness():\n # Override the waterfall mock to capture ctx\n with mock.patch.object(fetch_dc, \"_run_waterfall\",\n side_effect=capture):\n with mock.patch.object(\n fetch_dc.sys, \"argv\",\n [\"fetch_dc.py\", \"--session\", IDS.SID, \"--org\", \"my-org\",\n \"--verbose\", \"--no-assemble\", \"--no-render\"],\n ):\n fetch_dc.main()\n self.assertTrue(captured_ctx[\"verbose\"])\n\n\n# -----------------------------------------------------------------------------\n# Messaging-id input\n# -----------------------------------------------------------------------------\n\n\nclass MainMessagingIdTests(unittest.TestCase):\n\n def test_messaging_id_resolves_to_uuid_via_disk(self):\n # Fixture's session row has the messaging id wired up. Patch the\n # resolver at the source module so fetch_dc's lazy import sees it.\n with _MainHarness():\n with mock.patch(\n \"resolve_session.resolve_disk_or_live\",\n return_value=IDS.SID,\n ):\n with mock.patch.object(\n fetch_dc.sys, \"argv\",\n [\"fetch_dc.py\",\n \"--session\", \"0MwTESTMSG12345AAA\",\n \"--org\", \"my-org\",\n \"--no-assemble\", \"--no-render\"],\n ):\n rc = fetch_dc.main()\n self.assertEqual(rc, 0)\n\n\n# -----------------------------------------------------------------------------\n# DcAccessDenied → routes through _handle_dc_access_denied\n# -----------------------------------------------------------------------------\n\n\nclass MainDcAccessDeniedTests(unittest.TestCase):\n\n def test_returns_exit_dc_access_denied_in_headless(self):\n exc = fetch_dc.DcAccessDenied(\"401\", \"Unauthorized\")\n with _MainHarness(raise_dc_access_denied=exc, is_tty=False):\n with mock.patch.object(\n fetch_dc.sys, \"argv\",\n [\"fetch_dc.py\", \"--session\", IDS.SID, \"--org\", \"my-org\"],\n ):\n with mock.patch(\"sys.stdout\", io.StringIO()):\n rc = fetch_dc.main()\n self.assertEqual(rc, fetch_dc.EXIT_DC_ACCESS_DENIED)\n\n def test_returns_exit_dc_access_denied_in_tty_choice_1(self):\n exc = fetch_dc.DcAccessDenied(\"403\", \"Forbidden\")\n with _MainHarness(raise_dc_access_denied=exc, is_tty=True):\n with mock.patch.object(\n fetch_dc.sys, \"argv\",\n [\"fetch_dc.py\", \"--session\", IDS.SID, \"--org\", \"my-org\"],\n ):\n # Interactive prompt: feed \"1\" via stdin.\n with mock.patch.object(\n fetch_dc.sys, \"stdin\",\n io.StringIO(\"1\\n\"),\n ):\n with mock.patch.object(fetch_dc, \"_log\"):\n rc = fetch_dc.main()\n self.assertEqual(rc, fetch_dc.EXIT_DC_ACCESS_DENIED)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9620,"content_sha256":"daf6740aec7ab85734a272580a28564eb1e024a0c73dee8b120082504b821675"},{"filename":"scripts/tests/test_fetch_dc_waterfall_full.py","content":"\"\"\"End-to-end waterfall tests for ``fetch_dc._run_waterfall``.\n\nMocks ``dc.post`` at the network boundary; dispatches per-query fixture\nrows so the 5-wave orchestration runs to completion. Asserts on:\n\n- per-query manifest entries (rows, status, wave)\n- harvested_ids tracking across waves\n- session_shape classification\n- on-disk dc.\u003cname>.json artifacts written by ``storage.save``\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport unittest\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport fetch_dc # type: ignore\nfrom config import paths # type: ignore\nfrom .fixtures.synthetic_session import IDS, make_rows # type: ignore\n\n\ndef _build_fixture_dispatch() -> dict[str, list[dict]]:\n \"\"\"Return rows keyed by DMO name for the synthetic session fixture.\"\"\"\n return make_rows()\n\n\ndef _make_post_dispatcher(rows_by_name: dict[str, list[dict]]):\n \"\"\"Return a fake ``dc.post`` that dispatches to the right fixture\n rows by inspecting the query_name argument.\"\"\"\n def fake_post(sql: str, instance_url: str, token: str, query_name: str = \"\"):\n # `_fetch` passes the template name as query_name; that's exactly\n # what we keyed on in the fixture.\n return list(rows_by_name.get(query_name, []))\n return fake_post\n\n\ndef _make_ctx(tmpdir: Path) -> dict:\n \"\"\"Build the same ctx shape that fetch_dc.main() builds.\"\"\"\n return {\n \"session_id\": IDS.SID,\n \"org_alias\": \"my-org\",\n \"instance_url\": \"https://example.salesforce.com\",\n \"token\": \"TOKEN\",\n \"verbose\": False,\n \"queries\": [],\n \"started_at\": datetime.now(timezone.utc),\n \"org_id_15\": None,\n \"agent_api_name\": None,\n \"agent_version\": None,\n }\n\n\nclass _WaterfallHarness:\n \"\"\"Patch DATA_ROOT + dc.post; build ctx; run the waterfall.\"\"\"\n\n def __init__(self, *, rows_overrides: dict[str, list[dict]] | None = None):\n self.rows_overrides = rows_overrides or {}\n\n def __enter__(self):\n self._tmp = TemporaryDirectory()\n self._tmpdir = Path(self._tmp.name)\n self.ctx = _make_ctx(self._tmpdir)\n rows = _build_fixture_dispatch()\n rows.update(self.rows_overrides)\n self._post = _make_post_dispatcher(rows)\n self._patches = [\n mock.patch.object(paths, \"DATA_ROOT\", self._tmpdir),\n mock.patch.object(fetch_dc, \"post\", side_effect=self._post),\n # Keep _log quiet during tests\n mock.patch.object(fetch_dc, \"_log\"),\n ]\n for p in self._patches:\n p.start()\n return self\n\n def __exit__(self, *exc):\n for p in self._patches:\n p.stop()\n self._tmp.cleanup()\n\n def session_dir(self) -> Path:\n # macOS resolves /var → /private/var; resolve once so `is_file`\n # checks work after storage.save's path composition.\n return (\n self._tmpdir.resolve()\n / IDS.ORG_ID_15\n / f\"{IDS.AGENT_API}__{IDS.AGENT_VERSION}\"\n / IDS.SID\n )\n\n\n# -----------------------------------------------------------------------------\n# Identity stamping (wave 1a)\n# -----------------------------------------------------------------------------\n\n\nclass WaterfallIdentityTests(unittest.TestCase):\n\n def test_identity_resolved_into_ctx_after_wave_1a(self):\n with _WaterfallHarness() as h:\n fetch_dc._run_waterfall(h.ctx)\n self.assertEqual(h.ctx[\"org_id_15\"], IDS.ORG_ID_15)\n self.assertEqual(h.ctx[\"agent_api_name\"], IDS.AGENT_API)\n self.assertEqual(h.ctx[\"agent_version\"], IDS.AGENT_VERSION)\n\n def test_sessions_and_participants_persisted_after_identity_resolved(self):\n # The deferred-save flush in wave 1a should write both files.\n with _WaterfallHarness() as h:\n fetch_dc._run_waterfall(h.ctx)\n # Search the entire tmp tree — avoids macOS /private/var ↔ /var\n # resolution discrepancies between paths.session_dir() and our\n # locally-built path.\n written = {p.name for p in h._tmpdir.rglob(\"dc.*.json\")}\n self.assertIn(\"dc.sessions.json\", written)\n self.assertIn(\"dc.participants.json\", written)\n\n\n# -----------------------------------------------------------------------------\n# Wave-by-wave artifact persistence\n# -----------------------------------------------------------------------------\n\n\nclass WaterfallWritesArtifactsTests(unittest.TestCase):\n\n def test_populated_dmos_emit_dc_json_artifacts(self):\n with _WaterfallHarness() as h:\n fetch_dc._run_waterfall(h.ctx)\n files = {p.name for p in h._tmpdir.rglob(\"dc.*.json\")}\n # 11 DMOs are populated in the synthetic fixture; each lands as a file.\n for name in (\n \"dc.sessions.json\", \"dc.interactions.json\", \"dc.steps.json\",\n \"dc.messages.json\", \"dc.participants.json\",\n \"dc.gateway_requests.json\", \"dc.gateway_responses.json\",\n \"dc.generations.json\", \"dc.content_quality.json\",\n \"dc.gateway_records.json\", \"dc.gateway_request_tags.json\",\n ):\n self.assertIn(name, files, f\"{name} should be on disk\")\n\n def test_zero_row_dmos_skipped_no_artifact(self):\n with _WaterfallHarness() as h:\n fetch_dc._run_waterfall(h.ctx)\n files = {p.name for p in h._tmpdir.rglob(\"dc.*.json\")}\n # `tags` and friends have empty rows → no on-disk artifact.\n self.assertNotIn(\"dc.tags.json\", files)\n self.assertNotIn(\"dc.tag_definitions.json\", files)\n\n\n# -----------------------------------------------------------------------------\n# Manifest entries (one per query)\n# -----------------------------------------------------------------------------\n\n\nclass WaterfallManifestTests(unittest.TestCase):\n\n def test_24_query_entries_appended(self):\n with _WaterfallHarness() as h:\n fetch_dc._run_waterfall(h.ctx)\n names = [q[\"name\"] for q in h.ctx[\"queries\"]]\n # Exactly the 24 templates fetch_dc declares (no duplicates from\n # tag_definitions retry: synthetic fixture's tag_definitions is\n # empty, so retry runs once and overwrites the prior empty entry).\n self.assertGreaterEqual(len(names), 24)\n # Critical names appear at least once\n for required in (\"sessions\", \"interactions\", \"steps\", \"participants\",\n \"gateway_requests\", \"generations\", \"messages\"):\n self.assertIn(required, names)\n\n def test_each_query_has_wave_status_rows_elapsed_ms(self):\n with _WaterfallHarness() as h:\n fetch_dc._run_waterfall(h.ctx)\n for q in h.ctx[\"queries\"]:\n self.assertIn(\"name\", q)\n self.assertIn(\"wave\", q)\n self.assertIn(\"rows\", q)\n self.assertIn(\"status\", q)\n self.assertIn(\"elapsed_ms\", q)\n self.assertIn(q[\"status\"], (\"ok\", \"empty\", \"skipped\", \"error\"))\n\n\n# -----------------------------------------------------------------------------\n# harvested_ids\n# -----------------------------------------------------------------------------\n\n\nclass WaterfallHarvestedIdsTests(unittest.TestCase):\n\n def test_harvested_ids_match_fixture_counts(self):\n with _WaterfallHarness() as h:\n fetch_dc._run_waterfall(h.ctx)\n harvested = h.ctx[\"harvested_ids\"]\n self.assertEqual(harvested[\"sessions\"], 1)\n self.assertEqual(harvested[\"interactions\"], 3)\n self.assertEqual(harvested[\"steps_total\"], 3)\n self.assertEqual(harvested[\"gateway_request_ids\"], 2)\n # moments DMO is empty; fetch_dc falls through to the participants\n # harvest (AGENT participant row carries IDS.AGENT_API = \"DemoAgent\").\n from .fixtures.synthetic_session import IDS # type: ignore\n self.assertEqual(harvested[\"agents_observed\"], [IDS.AGENT_API])\n\n def test_steps_by_type_categorizes_correctly(self):\n with _WaterfallHarness() as h:\n fetch_dc._run_waterfall(h.ctx)\n sbt = h.ctx[\"harvested_ids\"][\"steps_by_type\"]\n # Synthetic fixture has 1 each of TOPIC_STEP, ACTION_STEP, TRUST_GUARDRAILS_STEP\n self.assertEqual(sbt[\"ACTION_STEP\"], 1)\n self.assertEqual(sbt[\"TOPIC_STEP\"], 1)\n self.assertEqual(sbt[\"TRUST_GUARDRAILS_STEP\"], 1)\n self.assertEqual(sbt[\"LLM_STEP\"], 0)\n\n\n# -----------------------------------------------------------------------------\n# Session-shape classification\n# -----------------------------------------------------------------------------\n\n\nclass WaterfallSessionShapeTests(unittest.TestCase):\n\n def test_normal_session_classified_as_complete(self):\n with _WaterfallHarness() as h:\n fetch_dc._run_waterfall(h.ctx)\n self.assertEqual(h.ctx[\"session_shape\"], \"complete\")\n\n def test_no_steps_with_gateway_requests_classified_as_lag(self):\n # Override: empty interactions/steps/messages but keep gateway_requests.\n rows = make_rows()\n rows[\"interactions\"] = []\n rows[\"steps\"] = []\n rows[\"messages\"] = []\n with _WaterfallHarness(rows_overrides=rows) as h:\n fetch_dc._run_waterfall(h.ctx)\n self.assertEqual(h.ctx[\"session_shape\"], \"interactions_not_materialized_yet\")\n\n def test_steps_no_llm_no_gateway_classified_as_abandoned(self):\n # ACTION_STEP + TOPIC_STEP exist but no LLM_STEP and no gateway_requests.\n rows = make_rows()\n rows[\"gateway_requests\"] = []\n rows[\"gateway_responses\"] = []\n rows[\"gateway_records\"] = []\n rows[\"gateway_request_tags\"] = []\n with _WaterfallHarness(rows_overrides=rows) as h:\n fetch_dc._run_waterfall(h.ctx)\n self.assertEqual(h.ctx[\"session_shape\"], \"abandoned_before_llm\")\n\n\n# -----------------------------------------------------------------------------\n# Empty harvest branches (zero IDs → _fetch_empty path)\n# -----------------------------------------------------------------------------\n\n\nclass WaterfallEmptyHarvestTests(unittest.TestCase):\n\n def test_no_interactions_skips_steps_via_fetch_empty(self):\n rows = make_rows()\n rows[\"interactions\"] = []\n with _WaterfallHarness(rows_overrides=rows) as h:\n fetch_dc._run_waterfall(h.ctx)\n # The steps query should be in the manifest as 'skipped'.\n steps_entries = [q for q in h.ctx[\"queries\"] if q[\"name\"] == \"steps\"]\n self.assertEqual(len(steps_entries), 1)\n self.assertEqual(steps_entries[0][\"status\"], \"skipped\")\n self.assertIn(\"no interactions\", steps_entries[0][\"_unavailable_reason\"])\n\n def test_no_gateway_requests_skips_audit_chain(self):\n rows = make_rows()\n rows[\"gateway_requests\"] = []\n with _WaterfallHarness(rows_overrides=rows) as h:\n fetch_dc._run_waterfall(h.ctx)\n # gateway_responses + tags + records + metadata + llm all skipped\n for name in (\"gateway_responses\", \"gateway_request_tags\",\n \"gateway_records\", \"gateway_request_metadata\",\n \"gateway_request_llm\"):\n entries = [q for q in h.ctx[\"queries\"] if q[\"name\"] == name]\n self.assertEqual(entries[0][\"status\"], \"skipped\",\n f\"{name} should be skipped\")\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":11477,"content_sha256":"02c9c827ff68ae746fc7df2e77429f23f7508afbf94e8a02536565f9d634d1fc"},{"filename":"scripts/tests/test_fetch_dc_waterfall.py","content":"\"\"\"Tests for ``fetch_dc`` waterfall building blocks.\n\nCovers the per-query helpers and the manifest writer. Doesn't drive the\nfull 5-wave waterfall (that's exercised live and the orchestration is\nlinear over the small per-query helpers tested here).\n\n- ``_classify_session_shape`` pure\n- ``_fetch`` HTTP + storage.save side effects\n- ``_fetch_empty`` no-HTTP record-only\n- ``_write_manifest`` on-disk JSON shape\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport unittest\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport fetch_dc # type: ignore\nfrom config import paths # type: ignore\nfrom dc import DCQueryError # type: ignore\nfrom .fixtures.synthetic_session import IDS # type: ignore\n\n\n# -----------------------------------------------------------------------------\n# _classify_session_shape — pure\n# -----------------------------------------------------------------------------\n\n\nclass ClassifySessionShapeTests(unittest.TestCase):\n\n def test_session_not_found_when_no_sessions(self):\n out = fetch_dc._classify_session_shape(\n sessions_count=0, steps_total=0,\n llm_step_count=0, gw_req_count=0,\n steps_with_generation_id=0,\n )\n self.assertEqual(out, \"session_not_found\")\n\n def test_interactions_not_materialized_yet_when_gw_present_no_steps(self):\n out = fetch_dc._classify_session_shape(\n sessions_count=1, steps_total=0,\n llm_step_count=0, gw_req_count=2,\n steps_with_generation_id=0,\n )\n self.assertEqual(out, \"interactions_not_materialized_yet\")\n\n def test_abandoned_before_llm_when_steps_present_no_llm(self):\n out = fetch_dc._classify_session_shape(\n sessions_count=1, steps_total=3,\n llm_step_count=0, gw_req_count=0,\n steps_with_generation_id=0,\n )\n self.assertEqual(out, \"abandoned_before_llm\")\n\n def test_planner_ran_no_gateway_logs(self):\n out = fetch_dc._classify_session_shape(\n sessions_count=1, steps_total=3,\n llm_step_count=2, gw_req_count=0,\n steps_with_generation_id=2,\n )\n self.assertEqual(out, \"planner_ran_no_gateway_logs\")\n\n def test_complete_for_normal_session(self):\n out = fetch_dc._classify_session_shape(\n sessions_count=1, steps_total=3,\n llm_step_count=1, gw_req_count=2,\n steps_with_generation_id=1,\n )\n self.assertEqual(out, \"complete\")\n\n\n# -----------------------------------------------------------------------------\n# _fetch — HTTP success / error paths\n# -----------------------------------------------------------------------------\n\n\ndef _make_ctx(tmpdir: Path) -> dict:\n return {\n \"session_id\": IDS.SID,\n \"org_alias\": \"my-org\",\n \"instance_url\": \"https://example.salesforce.com\",\n \"token\": \"TOKEN\",\n \"verbose\": False,\n \"queries\": [],\n \"started_at\": datetime.now(timezone.utc),\n \"org_id_15\": IDS.ORG_ID_15,\n \"agent_api_name\": IDS.AGENT_API,\n \"agent_version\": IDS.AGENT_VERSION,\n }\n\n\nclass FetchHappyPathTests(unittest.TestCase):\n\n def test_fetch_records_query_in_manifest_and_saves_rows(self):\n rows = [{\"ssot__Id__c\": \"x\"}]\n with TemporaryDirectory() as t:\n with mock.patch.object(paths, \"DATA_ROOT\", Path(t)):\n ctx = _make_ctx(Path(t))\n with mock.patch.object(fetch_dc, \"post\", return_value=rows):\n out = fetch_dc._fetch(\n ctx, wave=1, name=\"sessions\",\n where=\"ssot__Id__c = 'x'\",\n )\n self.assertEqual(out, rows)\n self.assertEqual(len(ctx[\"queries\"]), 1)\n entry = ctx[\"queries\"][0]\n self.assertEqual(entry[\"name\"], \"sessions\")\n self.assertEqual(entry[\"wave\"], 1)\n self.assertEqual(entry[\"rows\"], 1)\n self.assertEqual(entry[\"status\"], \"ok\")\n\n def test_fetch_records_empty_status_when_zero_rows_returned(self):\n with TemporaryDirectory() as t:\n with mock.patch.object(paths, \"DATA_ROOT\", Path(t)):\n ctx = _make_ctx(Path(t))\n with mock.patch.object(fetch_dc, \"post\", return_value=[]):\n out = fetch_dc._fetch(\n ctx, wave=1, name=\"sessions\",\n where=\"ssot__Id__c = 'x'\",\n )\n self.assertEqual(out, [])\n self.assertEqual(ctx[\"queries\"][0][\"status\"], \"empty\")\n self.assertIn(\"_unavailable_reason\", ctx[\"queries\"][0])\n\n def test_fetch_records_join_path_when_provided(self):\n with TemporaryDirectory() as t:\n with mock.patch.object(paths, \"DATA_ROOT\", Path(t)):\n ctx = _make_ctx(Path(t))\n with mock.patch.object(fetch_dc, \"post\", return_value=[{\"x\": 1}]):\n fetch_dc._fetch(\n ctx, wave=2, name=\"generations\",\n where=\"generationId__c = 'x'\",\n join_path=\"Step.GenerationId__c\",\n )\n self.assertEqual(\n ctx[\"queries\"][0][\"join_path\"], \"Step.GenerationId__c\",\n )\n\n\nclass FetchErrorPathTests(unittest.TestCase):\n\n def test_fetch_records_error_status_when_post_raises(self):\n ctx = _make_ctx(Path(\"/tmp\"))\n with mock.patch.object(\n fetch_dc, \"post\", side_effect=DCQueryError(\"http=500 ISE\\n body\"),\n ):\n out = fetch_dc._fetch(\n ctx, wave=1, name=\"sessions\",\n where=\"ssot__Id__c = 'x'\",\n )\n self.assertEqual(out, [])\n entry = ctx[\"queries\"][0]\n self.assertEqual(entry[\"status\"], \"error\")\n self.assertEqual(entry[\"rows\"], 0)\n self.assertIn(\"http=500 ISE\", entry[\"_unavailable_reason\"])\n\n\n# -----------------------------------------------------------------------------\n# _fetch_empty — record-only, no HTTP\n# -----------------------------------------------------------------------------\n\n\nclass FetchEmptyTests(unittest.TestCase):\n\n def test_fetch_empty_records_skipped_status_with_reason(self):\n ctx = _make_ctx(Path(\"/tmp\"))\n # Should not call HTTP at all.\n with mock.patch.object(fetch_dc, \"post\") as p:\n fetch_dc._fetch_empty(\n ctx, wave=2, name=\"generations\",\n reason=\"no parent ids harvested\",\n )\n p.assert_not_called()\n entry = ctx[\"queries\"][0]\n self.assertEqual(entry[\"name\"], \"generations\")\n self.assertEqual(entry[\"status\"], \"skipped\")\n self.assertEqual(entry[\"rows\"], 0)\n self.assertEqual(\n entry[\"_unavailable_reason\"], \"no parent ids harvested\",\n )\n\n\n# -----------------------------------------------------------------------------\n# _write_manifest — JSON shape on disk\n# -----------------------------------------------------------------------------\n\n\nclass WriteManifestTests(unittest.TestCase):\n\n def test_writes_manifest_at_canonical_path(self):\n with TemporaryDirectory() as t:\n with mock.patch.object(paths, \"DATA_ROOT\", Path(t)):\n ctx = _make_ctx(Path(t))\n ctx[\"queries\"] = [\n {\"name\": \"sessions\", \"wave\": 1, \"rows\": 1,\n \"elapsed_ms\": 50, \"status\": \"ok\"},\n ]\n ctx[\"session_shape\"] = \"complete\"\n ctx[\"harvested_ids\"] = {\"interactions\": [IDS.IXN_TURN]}\n fetch_dc._write_manifest(ctx)\n\n manifest_path = (\n Path(t)\n / IDS.ORG_ID_15\n / f\"{IDS.AGENT_API}__{IDS.AGENT_VERSION}\"\n / IDS.SID\n / \"dc._session_manifest.json\"\n )\n self.assertTrue(manifest_path.is_file())\n manifest = json.loads(manifest_path.read_text())\n self.assertEqual(manifest[\"session_id\"], IDS.SID)\n self.assertEqual(manifest[\"org_alias\"], \"my-org\")\n self.assertEqual(manifest[\"org_id_15\"], IDS.ORG_ID_15)\n self.assertEqual(manifest[\"agent_api_name\"], IDS.AGENT_API)\n self.assertEqual(manifest[\"agent_version\"], IDS.AGENT_VERSION)\n self.assertEqual(manifest[\"session_shape\"], \"complete\")\n self.assertEqual(len(manifest[\"queries\"]), 1)\n self.assertIn(\"started_at_utc\", manifest)\n self.assertIn(\"finished_at_utc\", manifest)\n self.assertIn(\"elapsed_ms\", manifest)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8781,"content_sha256":"d8b4bed261b0444b42395384b4fdb616b732fe670335881a8db58c6c79d4db9f"},{"filename":"scripts/tests/test_identity_coherence.py","content":"\"\"\"Top-level vs nested identity coherence under manifest-placeholder shapes.\n\nRegression coverage for the bug where ``fetch_dc._resolve_identity`` falls\nthrough its strict AGENT-participant pick (fetch_dc.py:570-597) on agent\nshapes like MyAgent — leaving the manifest with\n``agent_version=\"v0\"`` (placeholder) — while wave 5 then materializes\n``gateway_request_tags`` rows that carry the real ``v24``. The harvester\n``_build_session_identity`` correctly reads ``v24`` into\n``session.identity.agent_version``, but the top-level ``identity`` block\nwas previously copied straight off the manifest and silently disagreed\nwith the same JSON file's nested copy.\n\nThese tests pin the promotion policy implemented by\n``_promote_identity`` / ``_reconcile_top_identity``:\n\n - manifest \"v0\" + session \"v24\" -> top promotes to v24\n - manifest \"v3\" + session None -> top stays v3 (manifest wins)\n - manifest api None + session real -> top promotes\n - manifest \"v3\" + session \"v5\" -> top stays v3 (manifest wins;\n strict AGENT pick is intentional)\n - manifest \"v0\" + session \"v0\" -> top stays v0 (no real value\n available; placeholder leak\n remains visible to investigator)\n\nPlus a full round-trip through ``assemble_dc.assemble`` to confirm the\ntop-level and nested identity records agree on a real MyAgent-shape\nsession.\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport unittest\nfrom copy import deepcopy\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport assemble_dc # type: ignore\nfrom config import paths # type: ignore\nfrom .fixtures.synthetic_session import ( # type: ignore\n IDS, make_manifest, make_rows, write_to_disk,\n)\n\n\n# -----------------------------------------------------------------------------\n# Unit tests for _promote_identity (single-field policy)\n# -----------------------------------------------------------------------------\n\n\nclass PromoteVersionTests(unittest.TestCase):\n \"\"\"`_promote_identity(..., kind=\"version\")` — placeholder-only promotion.\"\"\"\n\n def test_v0_placeholder_promoted_when_session_has_real_version(self):\n out = assemble_dc._promote_identity(\"v0\", \"v24\", kind=\"version\")\n self.assertEqual(out, \"v24\")\n\n def test_keeps_manifest_value_when_session_identity_is_none(self):\n # Manifest already real; no reason to look elsewhere.\n out = assemble_dc._promote_identity(\"v3\", None, kind=\"version\")\n self.assertEqual(out, \"v3\")\n\n def test_keeps_v0_when_session_also_returns_v0(self):\n # No real value harvestable. Placeholder remains visible so\n # the investigator can still see the resolution failed.\n out = assemble_dc._promote_identity(\"v0\", \"v0\", kind=\"version\")\n self.assertEqual(out, \"v0\")\n\n def test_keeps_v0_when_session_value_is_garbage(self):\n # Session shouldn't be able to \"promote\" a non-version-shaped string.\n out = assemble_dc._promote_identity(\"v0\", \"garbage\", kind=\"version\")\n self.assertEqual(out, \"v0\")\n\n def test_keeps_v0_when_session_value_is_none(self):\n out = assemble_dc._promote_identity(\"v0\", None, kind=\"version\")\n self.assertEqual(out, \"v0\")\n\n def test_no_promotion_when_both_real_but_disagree(self):\n # Strict AGENT-row pick is intentional — manifest wins.\n out = assemble_dc._promote_identity(\"v3\", \"v5\", kind=\"version\")\n self.assertEqual(out, \"v3\")\n\n\nclass PromoteApiNameTests(unittest.TestCase):\n \"\"\"`_promote_identity(..., kind=\"api_name\")` — NOT_SET-ish promotion.\"\"\"\n\n def test_none_manifest_promoted_when_session_has_value(self):\n out = assemble_dc._promote_identity(\n None, \"MyAgent\", kind=\"api_name\",\n )\n self.assertEqual(out, \"MyAgent\")\n\n def test_not_set_string_promoted_when_session_has_value(self):\n out = assemble_dc._promote_identity(\n \"NOT_SET\", \"MyAgent\", kind=\"api_name\",\n )\n self.assertEqual(out, \"MyAgent\")\n\n def test_empty_manifest_promoted_when_session_has_value(self):\n out = assemble_dc._promote_identity(\"\", \"MyAgent\", kind=\"api_name\")\n self.assertEqual(out, \"MyAgent\")\n\n def test_keeps_manifest_value_when_session_is_none(self):\n # Symmetric of the version case — manifest wins when session\n # has nothing to offer.\n out = assemble_dc._promote_identity(\n \"MyAgent\", None, kind=\"api_name\",\n )\n self.assertEqual(out, \"MyAgent\")\n\n def test_keeps_manifest_when_both_real(self):\n out = assemble_dc._promote_identity(\n \"ManifestAgent\", \"SessionAgent\", kind=\"api_name\",\n )\n self.assertEqual(out, \"ManifestAgent\")\n\n\n# -----------------------------------------------------------------------------\n# Reconcile helper — composed policy across both fields + stderr note\n# -----------------------------------------------------------------------------\n\n\nclass ReconcileTopIdentityTests(unittest.TestCase):\n\n def test_reconcile_emits_stderr_note_when_version_promoted(self):\n manifest = {\"agent_api_name\": \"MyAgent\", \"agent_version\": \"v0\"}\n session_identity = {\n \"agent_api_name\": None,\n \"agent_version\": \"v24\",\n }\n with mock.patch(\"sys.stderr\") as fake_stderr:\n top = assemble_dc._reconcile_top_identity(\n manifest, session_identity, \"00DXX0000000ABC\",\n )\n self.assertEqual(top[\"agent_version\"], \"v24\")\n self.assertEqual(top[\"agent_api_name\"], \"MyAgent\")\n self.assertEqual(top[\"org_id_15\"], \"00DXX0000000ABC\")\n # The stderr note format is part of the contract — at least\n # one print call mentioning \"promoted\" must fire.\n emitted = [\n call.args[0] for call in fake_stderr.write.call_args_list\n if call.args\n ]\n self.assertTrue(\n any(\"promoted\" in str(s) for s in emitted),\n f\"expected promotion note on stderr; got writes: {emitted}\",\n )\n\n def test_reconcile_silent_when_no_promotion_needed(self):\n manifest = {\"agent_api_name\": \"DemoAgent\", \"agent_version\": \"v5\"}\n session_identity = {\n \"agent_api_name\": \"DemoAgent\",\n \"agent_version\": \"v5\",\n }\n with mock.patch(\"sys.stderr\") as fake_stderr:\n top = assemble_dc._reconcile_top_identity(\n manifest, session_identity, \"00DXX0000000ABC\",\n )\n self.assertEqual(top[\"agent_version\"], \"v5\")\n emitted = [\n call.args[0] for call in fake_stderr.write.call_args_list\n if call.args\n ]\n self.assertFalse(\n any(\"promoted\" in str(s) for s in emitted),\n f\"unexpected promotion note: {emitted}\",\n )\n\n\n# -----------------------------------------------------------------------------\n# Full-fixture round-trip — MyAgent-shape session through assemble()\n# -----------------------------------------------------------------------------\n\n\ndef _serviceagent2_shape_disk_writer(tmp_root: Path) -> Path:\n \"\"\"Materialize a MyAgent-shape session under tmp_root.\n\n Mutates the synthetic fixture so that:\n\n - manifest carries the placeholder ``agent_version=\"v0\"`` (mirrors\n what ``fetch_dc._resolve_identity``'s fallback stamps).\n - the AGENT participant row has its ``ssot__AiAgentVersionApiName__c``\n cleared to NOT_SET (so ``_build_session_identity`` doesn't pick up\n a version from there).\n - ``gateway_request_tags`` carries an ``agent_version_api_name=v24``\n tag against the declared GW request, so the harvester finds the\n real value.\n\n The session dir lives under ``\u003corg>/\u003capi_name>__v0/\u003csid>/`` to match the\n placeholder-stamped layout fetch_dc actually writes on this path.\n \"\"\"\n rows = make_rows()\n\n # AGENT row: clear version so _build_session_identity has no hint\n # via that path; the harvest must come from gateway_request_tags.\n for p in rows[\"participants\"]:\n if p.get(\"ssot__AiAgentSessionParticipantRole__c\") == \"AGENT\":\n p[\"ssot__AiAgentVersionApiName__c\"] = \"NOT_SET\"\n\n # Add the agent_version_api_name tag against the declared GW request.\n rows[\"gateway_request_tags\"].append({\n \"id__c\": \"tag-agent-version\",\n \"parent__c\": IDS.GW_REQ_DECLARED,\n \"tag__c\": \"agent_version_api_name\",\n \"tagValue__c\": '\"v24\"', # quoted form mirrors live shape\n })\n # Plus agent_developer_name so session.identity.agent_api_name\n # also gets a real value to test the api_name path.\n rows[\"gateway_request_tags\"].append({\n \"id__c\": \"tag-agent-dev\",\n \"parent__c\": IDS.GW_REQ_DECLARED,\n \"tag__c\": \"agent_developer_name\",\n \"tagValue__c\": '\"MyAgent\"',\n })\n\n # Manifest: placeholder version, real api_name (matches the\n # MyAgent fallback's actual output — it stamps api_name from\n # any participant row but version is forced to \"v0\").\n manifest = make_manifest()\n manifest[\"agent_api_name\"] = \"MyAgent\"\n manifest[\"agent_version\"] = \"v0\"\n manifest[\"org_id_15\"] = IDS.ORG_ID_15\n\n # Layout: \u003corg>/\u003capi_name>__v0/\u003csid>/ — the placeholder-shaped dir.\n sdir = tmp_root / IDS.ORG_ID_15 / \"MyAgent__v0\" / IDS.SID\n sdir.mkdir(parents=True, exist_ok=True)\n\n (sdir / \"dc._session_manifest.json\").write_text(\n json.dumps(manifest, indent=2) + \"\\n\"\n )\n for name, rs in rows.items():\n if rs:\n (sdir / f\"dc.{name}.json\").write_text(\n json.dumps(rs, indent=2) + \"\\n\"\n )\n\n # Breadcrumb for _find_session_dir's primary path.\n link_dir = tmp_root / IDS.ORG_ID_15 / \"_sessions\"\n link_dir.mkdir(parents=True, exist_ok=True)\n (link_dir / f\"{IDS.SID}.link\").write_text(\n f\"../MyAgent__v0/{IDS.SID}\\n\"\n )\n return sdir\n\n\nclass AssembleEndToEndIdentityCoherenceTests(unittest.TestCase):\n \"\"\"The bug repro — top-level vs nested identity must agree after fix.\"\"\"\n\n def test_top_identity_promoted_when_manifest_has_v0_and_tags_have_v24(self):\n with TemporaryDirectory() as tmp:\n tmp_root = Path(tmp)\n _serviceagent2_shape_disk_writer(tmp_root)\n with mock.patch.object(paths, \"DATA_ROOT\", tmp_root):\n tree, _ = assemble_dc.assemble(IDS.SID)\n # The harvester (_build_session_identity) reads v24 from tags.\n self.assertEqual(tree[\"session\"][\"identity\"][\"agent_version\"], \"v24\")\n # The top-level — previously v0 — must now agree.\n self.assertEqual(tree[\"identity\"][\"agent_version\"], \"v24\")\n\n def test_top_and_session_identity_agree_on_agent_version(self):\n with TemporaryDirectory() as tmp:\n tmp_root = Path(tmp)\n _serviceagent2_shape_disk_writer(tmp_root)\n with mock.patch.object(paths, \"DATA_ROOT\", tmp_root):\n tree, _ = assemble_dc.assemble(IDS.SID)\n # The whole point of the fix.\n self.assertEqual(\n tree[\"identity\"][\"agent_version\"],\n tree[\"session\"][\"identity\"][\"agent_version\"],\n )\n\n def test_api_name_top_level_keeps_manifest_when_session_returns_null(self):\n # Mirror the live live-verification observation: top-level\n # api_name is \"MyAgent\" (from manifest), session.identity\n # api_name is None because no agent_developer_name tag fired.\n # Strip the agent_developer_name tag we added in the helper.\n with TemporaryDirectory() as tmp:\n tmp_root = Path(tmp)\n _serviceagent2_shape_disk_writer(tmp_root)\n sdir = tmp_root / IDS.ORG_ID_15 / \"MyAgent__v0\" / IDS.SID\n tags_path = sdir / \"dc.gateway_request_tags.json\"\n tags = json.loads(tags_path.read_text())\n tags = [t for t in tags if t.get(\"tag__c\") != \"agent_developer_name\"]\n tags_path.write_text(json.dumps(tags, indent=2) + \"\\n\")\n\n with mock.patch.object(paths, \"DATA_ROOT\", tmp_root):\n tree, _ = assemble_dc.assemble(IDS.SID)\n # session.identity.agent_api_name None — no harvestable value.\n self.assertIsNone(tree[\"session\"][\"identity\"][\"agent_api_name\"])\n # Top-level keeps manifest's \"MyAgent\" — promotion does NOT\n # erase a real manifest value when session has nothing to offer.\n self.assertEqual(tree[\"identity\"][\"agent_api_name\"], \"MyAgent\")\n\n\n# -----------------------------------------------------------------------------\n# Sanity: the existing happy-path fixture still produces matching identity\n# (regression guard against the fix mutating the simple case).\n# -----------------------------------------------------------------------------\n\n\nclass HappyPathStillCoherentTests(unittest.TestCase):\n\n def test_happy_path_unchanged(self):\n # The synthetic fixture's gateway_request_tags don't include\n # `agent_version_api_name`, so session.identity.agent_version is\n # None on this path. The fix's policy preserves the manifest\n # value (\"v5\") in that case — verifying the no-promote branch\n # doesn't accidentally null out the top-level when the harvest\n # layer has nothing to offer.\n with TemporaryDirectory() as tmp:\n tmp_root = Path(tmp)\n write_to_disk(tmp_root)\n with mock.patch.object(paths, \"DATA_ROOT\", tmp_root):\n tree, _ = assemble_dc.assemble(IDS.SID)\n self.assertEqual(tree[\"identity\"][\"agent_version\"], IDS.AGENT_VERSION)\n self.assertEqual(tree[\"identity\"][\"agent_api_name\"], IDS.AGENT_API)\n # session.identity may be None here (no agent_version_api_name tag);\n # the top-level keeps the manifest's real value, which is the point.\n self.assertIn(\n tree[\"session\"][\"identity\"][\"agent_version\"],\n (None, IDS.AGENT_VERSION),\n )\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":14106,"content_sha256":"acbd7f6670bb93cd4c5139faa6e8ce58e0d382a869e0c501ac6360b109d6cfe3"},{"filename":"scripts/tests/test_render_dc_branches.py","content":"\"\"\"Additional ``render_dc`` tests targeting branches not covered by\n``test_render_dc_integration.py``:\n\n- ``main_for_session`` reads tree + manifest from disk; emits markdown\n- ``main`` argparse → main_for_session\n- ``_parse_finish_reason`` HTML-escaped JSON parsing\n- ``_decoded_line`` tool-call JSON detection\n- mermaid sequence diagram (requires ≥2 ACTION_STEPs OR an error_text)\n- mermaid topic flowchart (requires repeating topics across turns)\n- schema-version refusal on unsupported _schema_version\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport unittest\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport assemble_dc # type: ignore\nimport render_dc # type: ignore\nfrom config import paths # type: ignore\nfrom .fixtures.synthetic_session import IDS, write_to_disk # type: ignore\n\n\n# -----------------------------------------------------------------------------\n# main_for_session — disk-backed entry point\n# -----------------------------------------------------------------------------\n\n\nclass MainForSessionTests(unittest.TestCase):\n\n def test_reads_tree_writes_summary_md(self):\n with TemporaryDirectory() as t:\n tmp = Path(t)\n sdir = write_to_disk(tmp)\n with mock.patch.object(paths, \"DATA_ROOT\", tmp):\n tree, _ = assemble_dc.assemble(IDS.SID)\n (sdir / \"dc._session_tree.json\").write_text(\n json.dumps(tree, default=str)\n )\n rc = render_dc.main_for_session(IDS.SID)\n self.assertEqual(rc, 0)\n summary_path = sdir / \"dc._session_summary.md\"\n self.assertTrue(summary_path.is_file())\n self.assertGreater(len(summary_path.read_text()), 1000)\n\n def test_raises_when_tree_missing(self):\n with TemporaryDirectory() as t:\n tmp = Path(t)\n write_to_disk(tmp) # creates session dir but not tree\n with mock.patch.object(paths, \"DATA_ROOT\", tmp):\n with self.assertRaises(SystemExit) as ctx:\n render_dc.main_for_session(IDS.SID)\n self.assertIn(\"tree not found\", str(ctx.exception))\n\n def test_tolerates_missing_manifest_with_warning(self):\n with TemporaryDirectory() as t:\n tmp = Path(t)\n sdir = write_to_disk(tmp)\n with mock.patch.object(paths, \"DATA_ROOT\", tmp):\n tree, _ = assemble_dc.assemble(IDS.SID)\n (sdir / \"dc._session_tree.json\").write_text(\n json.dumps(tree, default=str)\n )\n # Wipe the manifest. main_for_session should still succeed\n # (manifest is optional; missing → no manifest-driven sections).\n (sdir / \"dc._session_manifest.json\").unlink()\n rc = render_dc.main_for_session(IDS.SID)\n self.assertEqual(rc, 0)\n\n\n# -----------------------------------------------------------------------------\n# main — argparse + resolve\n# -----------------------------------------------------------------------------\n\n\nclass MainTests(unittest.TestCase):\n\n def test_main_invokes_main_for_session_with_resolved_sid(self):\n with TemporaryDirectory() as t:\n tmp = Path(t)\n sdir = write_to_disk(tmp)\n with mock.patch.object(paths, \"DATA_ROOT\", tmp):\n tree, _ = assemble_dc.assemble(IDS.SID)\n (sdir / \"dc._session_tree.json\").write_text(\n json.dumps(tree, default=str)\n )\n with mock.patch.object(\n render_dc.sys, \"argv\",\n [\"render_dc.py\", \"--session\", IDS.SID],\n ):\n rc = render_dc.main()\n self.assertEqual(rc, 0)\n\n\n# -----------------------------------------------------------------------------\n# _parse_finish_reason — HTML-escaped JSON\n# -----------------------------------------------------------------------------\n\n\nclass ParseFinishReasonTests(unittest.TestCase):\n\n def test_extracts_stop_from_double_escaped_json(self):\n # Real wire format: `{"finish_reason":"\\\"stop\\\""}`\n params = '{"finish_reason":"\\\\\"stop\\\\\""}'\n self.assertEqual(render_dc._parse_finish_reason(params), \"stop\")\n\n def test_returns_none_for_empty(self):\n self.assertIsNone(render_dc._parse_finish_reason(None))\n self.assertIsNone(render_dc._parse_finish_reason(\"\"))\n\n def test_returns_none_for_unparseable_json(self):\n self.assertIsNone(render_dc._parse_finish_reason(\"\u003c\u003c\u003cnot json>>>\"))\n\n def test_returns_none_when_finish_reason_not_string(self):\n params = '{"finish_reason": 42}'\n self.assertIsNone(render_dc._parse_finish_reason(params))\n\n\n# -----------------------------------------------------------------------------\n# _decoded_line — tool-call JSON detection\n# -----------------------------------------------------------------------------\n\n\nclass DecodedLineTests(unittest.TestCase):\n\n def test_returns_dash_for_empty(self):\n self.assertEqual(render_dc._decoded_line(None), \"—\")\n self.assertEqual(render_dc._decoded_line(\"\"), \"—\")\n\n def test_renders_quoted_truncated_text_for_plain_string(self):\n out = render_dc._decoded_line(\"Hello world\")\n self.assertEqual(out, '\"Hello world\"')\n\n def test_summarizes_tool_call_json(self):\n # Real wire shape: HTML-escaped JSON with toolInvocations array.\n body = (\n '{\"toolInvocations\":[{'\n '\"function\":{\"name\":\"search_orders\",'\n '\"arguments\":\"{\\\\\"query\\\\\":\\\\\"refund\\\\\"}\"}}],'\n '\"content\":\"I will search for orders.\"}'\n )\n out = render_dc._decoded_line(body)\n self.assertIn(\"1 tool call\", out)\n self.assertIn(\"search_orders\", out)\n self.assertIn(\"query=\", out)\n\n\n# -----------------------------------------------------------------------------\n# Multi-turn fixture for mermaid branches\n# -----------------------------------------------------------------------------\n\n\ndef _add_extra_turn_to_tree(tree: dict) -> None:\n \"\"\"Mutate the assembled tree to have a 2nd TURN with a topic that\n repeats — triggers both the sequence diagram and the topic flowchart.\n \"\"\"\n # Tag the existing turn with a topic\n sess = tree[\"session\"]\n for iv in sess[\"interactions\"]:\n if iv[\"type\"] == \"TURN\":\n iv[\"topic\"] = \"Refunds\"\n # Inject a 2nd action step so sequence diagram passes its\n # has_error/len(actions)>=2 gate.\n iv[\"steps\"].append({\n \"id\": \"step-extra-action\",\n \"type\": \"ACTION_STEP\",\n \"name\": \"ExtraAction\",\n \"start_ts\": \"2026-04-22T10:00:42.000Z\",\n \"end_ts\": \"2026-04-22T10:00:48.000Z\",\n \"error_text\": None,\n \"generation\": None,\n \"gateway_request\": None,\n })\n break\n # Append a 2nd TURN that uses the same topic — triggers flowchart edges.\n sess[\"interactions\"].append({\n \"id\": \"ixn-turn-002b\",\n \"type\": \"TURN\",\n \"topic\": \"Refunds\",\n \"trace_id\": \"trace-2nd\",\n \"start_ts\": \"2026-04-22T10:01:05.000Z\",\n \"end_ts\": \"2026-04-22T10:01:25.000Z\",\n \"steps\": [\n {\"id\": \"step-2a\", \"type\": \"ACTION_STEP\",\n \"name\": \"ActionA\",\n \"start_ts\": \"2026-04-22T10:01:08.000Z\",\n \"end_ts\": \"2026-04-22T10:01:12.000Z\",\n \"error_text\": None,\n \"generation\": None,\n \"gateway_request\": None},\n {\"id\": \"step-2b\", \"type\": \"ACTION_STEP\",\n \"name\": \"ActionB\",\n \"start_ts\": \"2026-04-22T10:01:13.000Z\",\n \"end_ts\": \"2026-04-22T10:01:18.000Z\",\n \"error_text\": \"Action B failed: timeout\",\n \"generation\": None,\n \"gateway_request\": None},\n ],\n \"messages\": [\n {\"role\": \"USER\", \"text\": \"I want to escalate.\", \"ts\": None},\n {\"role\": \"AGENT\", \"text\": \"Connecting you to a human.\", \"ts\": None},\n ],\n \"tag_associations\": [], \"telemetry_spans\": [],\n \"timestamp_bound_gateway_calls\": [],\n })\n\n\nclass MultiTurnRenderTests(unittest.TestCase):\n\n def _assemble_and_render(self, tree_mutator):\n with TemporaryDirectory() as t:\n tmp = Path(t)\n sdir = write_to_disk(tmp)\n with mock.patch.object(paths, \"DATA_ROOT\", tmp):\n tree, _ = assemble_dc.assemble(IDS.SID)\n tree_mutator(tree)\n manifest = json.loads(\n (sdir / \"dc._session_manifest.json\").read_text()\n )\n return render_dc.render(tree, manifest=manifest, session_dir=sdir)\n\n def test_sequence_diagram_emitted_when_turn_has_two_actions(self):\n md = self._assemble_and_render(_add_extra_turn_to_tree)\n self.assertIn(\"sequenceDiagram\", md)\n self.assertIn(\"participant U as USER\", md)\n self.assertIn(\"participant P as Planner\", md)\n\n def test_topic_flowchart_emitted_when_topics_repeat(self):\n md = self._assemble_and_render(_add_extra_turn_to_tree)\n self.assertIn(\"flowchart LR\", md)\n self.assertIn(\"Refunds\", md)\n\n\n# -----------------------------------------------------------------------------\n# Schema-version guard\n# -----------------------------------------------------------------------------\n\n\nclass SchemaVersionGuardTests(unittest.TestCase):\n\n def test_render_refuses_unsupported_schema_version(self):\n # _assert_schema_version reads session._schema_version (per impl).\n bad_tree = {\n \"session\": {\"id\": IDS.SID, \"interactions\": [], \"_schema_version\": 999},\n }\n with self.assertRaises(SystemExit) as ctx:\n render_dc.render(bad_tree)\n self.assertIn(\"unsupported tree _schema_version\", str(ctx.exception))\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10183,"content_sha256":"d0fca1c022c57c48c41372ce934abfc0e51d461f3ea85da993b07741c17d364b"},{"filename":"scripts/tests/test_render_dc_gateway_direct.py","content":"\"\"\"Tests for ``render_dc._render_gateway_direct``.\n\nFeeds a gateway-direct tree (built by ``_assemble_gateway_direct``)\nthrough the public ``render()`` entry point and asserts on the emitted\nmarkdown. The intent is to pin the section contract without coupling to\nexact whitespace:\n\n - lag banner appears\n - gateway chain table has the expected header\n - one H4 per gateway_request_id in per-call detail\n - per-call detail includes a fenced code block for the prompt\n - Interaction-dependent sections are ABSENT (Hierarchical trace,\n Per-turn summary, Transcript, Session counts)\n\"\"\"\nfrom __future__ import annotations\n\nimport unittest\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nfrom assemble_dc import _assemble_gateway_direct # type: ignore\nfrom render_dc import render # type: ignore\n\nfrom .test_assemble_dc_gateway_direct import (\n _SID,\n _make_manifest,\n _make_rows,\n)\n\n\nclass RenderGatewayDirectTests(unittest.TestCase):\n\n def setUp(self):\n rows = _make_rows()\n manifest = _make_manifest()\n self.tree = _assemble_gateway_direct(_SID, rows, manifest, parse_warnings=[])\n # Opt into prompt rendering — these tests exercise the *content* of\n # the per-call detail block, not the default-gating contract. The\n # default-gating tests live in test_render_dc_show_prompts_gating.py.\n self.md = render(self.tree, manifest, show_prompts=True)\n\n def test_starts_with_session_heading(self):\n self.assertTrue(self.md.startswith(f\"# Session {_SID}\"))\n\n def test_lag_banner_present(self):\n self.assertIn(\"STDM Interaction/Step/Message DMOs have not yet \"\n \"materialized\", self.md)\n self.assertIn(\"Re-run in 24–72h\", self.md)\n\n def test_gateway_chain_table_header(self):\n self.assertIn(\"| Model | Provider\", self.md)\n self.assertIn(\"Prompt tok\", self.md)\n self.assertIn(\"Response ts\", self.md)\n\n def test_h4_per_gateway_call(self):\n \"\"\"3 fixture gateway_requests → 3 `#### LLM call N` headings.\"\"\"\n h4_count = self.md.count(\"#### LLM call \")\n self.assertGreaterEqual(h4_count, 3)\n\n def test_prompt_fenced_code_block(self):\n \"\"\"Per-call detail wraps the prompt in a fenced code block.\"\"\"\n self.assertIn(\"```\", self.md)\n self.assertIn(\"This is the prompt body for gw-1.\", self.md)\n\n def test_no_hierarchical_trace_section(self):\n self.assertNotIn(\"Hierarchical trace\", self.md)\n self.assertNotIn(\"Complete hierarchical trace\", self.md)\n\n def test_no_per_turn_summary_section(self):\n self.assertNotIn(\"Per-turn summary\", self.md)\n\n def test_no_transcript_section(self):\n self.assertNotIn(\"## Transcript\", self.md)\n\n def test_session_identity_section_present(self):\n self.assertIn(\"## Session identity\", self.md)\n\n def test_prompt_with_triple_backticks_uses_longer_fence(self):\n \"\"\"Prompts containing ``` must get a 4+-backtick fence.\n\n LLM prompt templates commonly embed triple-backtick tool-use\n examples. A hardcoded ``` fence would close early and corrupt\n every section after the first such prompt.\n \"\"\"\n rows = _make_rows()\n rows[\"gateway_requests\"][0][\"prompt__c\"] = (\n \"See this example:\\n\"\n \"```python\\n\"\n \"print('hi')\\n\"\n \"```\\n\"\n \"End of example.\"\n )\n manifest = _make_manifest()\n tree = _assemble_gateway_direct(_SID, rows, manifest, parse_warnings=[])\n md = render(tree, manifest, show_prompts=True)\n self.assertIn(\"````\", md) # 4-backtick fence\n self.assertIn(\"print('hi')\", md)\n # The inner triple-backticks survive intact.\n self.assertIn(\"```python\", md)\n\n def test_prompt_with_four_backticks_uses_five_backtick_fence(self):\n \"\"\"Fence length scales with the longest inner run of backticks.\"\"\"\n rows = _make_rows()\n rows[\"gateway_requests\"][0][\"prompt__c\"] = (\n \"Edge case:\\n````\\nnested\\n````\\nDone.\"\n )\n manifest = _make_manifest()\n tree = _assemble_gateway_direct(_SID, rows, manifest, parse_warnings=[])\n md = render(tree, manifest, show_prompts=True)\n self.assertIn(\"`````\", md) # 5-backtick fence\n\n def test_prompt_truncation_marker_for_oversized_prompt(self):\n \"\"\"Display-only 64 KB cap: prompts over the limit carry a truncation marker.\n\n Raw JSON on disk is untouched — the cap only applies to markdown.\n \"\"\"\n rows = _make_rows()\n # Inflate the first prompt to 80 KB of ASCII.\n rows[\"gateway_requests\"][0][\"prompt__c\"] = \"x\" * 80_000\n manifest = _make_manifest()\n tree = _assemble_gateway_direct(_SID, rows, manifest, parse_warnings=[])\n md = render(tree, manifest, show_prompts=True)\n self.assertIn(\"[truncated; full prompt in dc.gateway_requests.json]\", md)\n # The on-tree prompt_text is still the full 80 KB — the cap is render-only.\n self.assertEqual(len(tree[\"session\"][\"gateway_chain\"][0][\"prompt_text\"]),\n 80_000)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5193,"content_sha256":"e011bfd3e514791c096dbbcf93fbb0ac1440ead4f55a23284c0ec033619ef3fc"},{"filename":"scripts/tests/test_render_dc_helpers.py","content":"\"\"\"Tests for ``render_dc`` pure-function helpers.\n\nThe renderer is large (863 stmts); covering its end-to-end markdown\noutput requires a complex tree fixture. These tests target the small,\npure helpers — they're cheap to test and account for ~150 stmts of the\ngap.\n\"\"\"\nfrom __future__ import annotations\n\nimport unittest\nfrom datetime import datetime, timezone\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport render_dc # type: ignore\n\n\n# -----------------------------------------------------------------------------\n# _parse_iso\n# -----------------------------------------------------------------------------\n\n\nclass ParseIsoTests(unittest.TestCase):\n\n def test_returns_none_for_falsy_input(self):\n self.assertIsNone(render_dc._parse_iso(None))\n self.assertIsNone(render_dc._parse_iso(\"\"))\n\n def test_parses_z_terminated(self):\n self.assertEqual(\n render_dc._parse_iso(\"2026-04-22T10:00:00Z\"),\n datetime(2026, 4, 22, 10, 0, 0, tzinfo=timezone.utc),\n )\n\n def test_returns_none_for_unparseable(self):\n self.assertIsNone(render_dc._parse_iso(\"not-a-timestamp\"))\n\n\n# -----------------------------------------------------------------------------\n# _fmt_offset\n# -----------------------------------------------------------------------------\n\n\nclass FmtOffsetTests(unittest.TestCase):\n\n def test_returns_dash_when_timestamp_or_start_missing(self):\n anchor = datetime(2026, 4, 22, 10, 0, 0, tzinfo=timezone.utc)\n self.assertEqual(render_dc._fmt_offset(None, anchor), \"—\")\n self.assertEqual(render_dc._fmt_offset(\"2026-04-22T10:00:00Z\", None), \"—\")\n\n def test_formats_offset_with_3_decimals(self):\n anchor = datetime(2026, 4, 22, 10, 0, 0, tzinfo=timezone.utc)\n out = render_dc._fmt_offset(\"2026-04-22T10:00:01.234Z\", anchor)\n self.assertEqual(out, \"+1.234s\")\n\n\n# -----------------------------------------------------------------------------\n# _fmt_duration_ms\n# -----------------------------------------------------------------------------\n\n\nclass FmtDurationMsTests(unittest.TestCase):\n\n def test_dash_when_either_side_missing(self):\n self.assertEqual(render_dc._fmt_duration_ms(None, \"2026-04-22T10:00:00Z\"), \"—\")\n self.assertEqual(render_dc._fmt_duration_ms(\"2026-04-22T10:00:00Z\", None), \"—\")\n\n def test_returns_int_ms_with_unit(self):\n out = render_dc._fmt_duration_ms(\n \"2026-04-22T10:00:00.000Z\", \"2026-04-22T10:00:00.123Z\",\n )\n self.assertEqual(out, \"123ms\")\n\n\n# -----------------------------------------------------------------------------\n# _decode + _truncate\n# -----------------------------------------------------------------------------\n\n\nclass DecodeTruncateTests(unittest.TestCase):\n\n def test_decode_unescapes_html_and_collapses_newlines(self):\n self.assertEqual(render_dc._decode(\""hi"\\nthere\"), '\"hi\" there')\n\n def test_decode_returns_empty_for_falsy(self):\n self.assertEqual(render_dc._decode(None), \"\")\n self.assertEqual(render_dc._decode(\"\"), \"\")\n\n def test_truncate_pads_with_ellipsis_when_over_n(self):\n long = \"a\" * 100\n out = render_dc._truncate(long, n=10)\n self.assertEqual(len(out), 10)\n self.assertTrue(out.endswith(\"…\"))\n\n def test_truncate_returns_input_when_under_or_equal_n(self):\n self.assertEqual(render_dc._truncate(\"short\", n=10), \"short\")\n\n def test_truncate_returns_dash_for_falsy(self):\n self.assertEqual(render_dc._truncate(None), \"—\")\n self.assertEqual(render_dc._truncate(\"\"), \"—\")\n\n\n# -----------------------------------------------------------------------------\n# _short\n# -----------------------------------------------------------------------------\n\n\nclass ShortTests(unittest.TestCase):\n\n def test_returns_dash_for_falsy(self):\n self.assertEqual(render_dc._short(None), \"—\")\n self.assertEqual(render_dc._short(\"\"), \"—\")\n\n def test_truncates_long_uuid_with_ellipsis(self):\n out = render_dc._short(\"019dface-0000-7000-8000-000000000001\", keep=8)\n self.assertEqual(out, \"019dface…\")\n\n def test_returns_input_when_short(self):\n self.assertEqual(render_dc._short(\"abc\"), \"abc\")\n\n\n# -----------------------------------------------------------------------------\n# _fmt_total_duration — h / m / s rollover\n# -----------------------------------------------------------------------------\n\n\nclass FmtTotalDurationTests(unittest.TestCase):\n\n def test_returns_none_when_either_side_missing(self):\n self.assertIsNone(render_dc._fmt_total_duration(None, \"2026-04-22T10:00:00Z\"))\n\n def test_seconds_only(self):\n self.assertEqual(\n render_dc._fmt_total_duration(\n \"2026-04-22T10:00:00.000Z\", \"2026-04-22T10:00:42.500Z\",\n ),\n \"42.500s\",\n )\n\n def test_minutes_and_seconds(self):\n self.assertEqual(\n render_dc._fmt_total_duration(\n \"2026-04-22T10:00:00Z\", \"2026-04-22T10:02:30Z\",\n ),\n \"2m 30.000s\",\n )\n\n def test_hours_minutes_seconds(self):\n out = render_dc._fmt_total_duration(\n \"2026-04-22T10:00:00Z\", \"2026-04-22T11:30:45Z\",\n )\n self.assertEqual(out, \"1h 30m 45.000s\")\n\n\n# -----------------------------------------------------------------------------\n# _derive_session_end\n# -----------------------------------------------------------------------------\n\n\nclass DeriveSessionEndTests(unittest.TestCase):\n\n def test_returns_session_end_ts_verbatim_when_present(self):\n end, source = render_dc._derive_session_end(\n {\"end_ts\": \"2026-04-22T11:00:00Z\", \"interactions\": []},\n )\n self.assertEqual(end, \"2026-04-22T11:00:00Z\")\n self.assertIsNone(source)\n\n def test_uses_session_end_interaction_start_ts(self):\n sess = {\n \"end_ts\": None,\n \"interactions\": [\n {\"type\": \"TURN\", \"start_ts\": \"10:00\", \"end_ts\": \"10:01\"},\n {\"type\": \"SESSION_END\", \"start_ts\": \"11:00:00Z\"},\n ],\n }\n end, source = render_dc._derive_session_end(sess)\n self.assertEqual(end, \"11:00:00Z\")\n self.assertEqual(source, \"from SESSION_END interaction\")\n\n def test_falls_back_to_last_turn_end_ts(self):\n sess = {\n \"end_ts\": None,\n \"interactions\": [\n {\"type\": \"TURN\", \"start_ts\": \"10:00\", \"end_ts\": \"10:01\"},\n {\"type\": \"TURN\", \"start_ts\": \"10:02\", \"end_ts\": \"10:03\"},\n ],\n }\n end, source = render_dc._derive_session_end(sess)\n self.assertEqual(end, \"10:03\")\n self.assertEqual(source, \"session still open (last TURN)\")\n\n def test_returns_none_pair_when_no_data(self):\n end, source = render_dc._derive_session_end({\"interactions\": []})\n self.assertIsNone(end)\n self.assertIsNone(source)\n\n\n# -----------------------------------------------------------------------------\n# _compose_agent_cell\n# -----------------------------------------------------------------------------\n\n\nclass ComposeAgentCellTests(unittest.TestCase):\n\n def test_full_identity_concatenated(self):\n out = render_dc._compose_agent_cell({\n \"agent_api_name\": \"MyAgent\",\n \"agent_version\": \"v3\",\n \"agent_label\": \"My Agent\",\n \"agent_type\": \"Internal\",\n })\n self.assertEqual(out, \"MyAgent v3 — My Agent (Internal)\")\n\n def test_only_api_name(self):\n out = render_dc._compose_agent_cell({\"agent_api_name\": \"MyAgent\"})\n self.assertEqual(out, \"MyAgent\")\n\n def test_only_label(self):\n out = render_dc._compose_agent_cell({\"agent_label\": \"Display Only\"})\n self.assertEqual(out, \"Display Only\")\n\n def test_returns_none_for_empty_identity(self):\n self.assertIsNone(render_dc._compose_agent_cell({}))\n\n\n# -----------------------------------------------------------------------------\n# _section_session_bootstrap — channel-aware NOT_SET wording\n# -----------------------------------------------------------------------------\n\n\nclass SectionSessionBootstrapNotSetWordingTests(unittest.TestCase):\n \"\"\"`VariableText__c == NOT_SET` is a \"no bootstrap variables\" signal,\n not a messaging-channel signal. Several non-messaging shapes (E&O\n headless API, Atlas planner, generic API/integration) legitimately\n produce NOT_SET — the rendered line must not call those \"production\n messaging path\".\n \"\"\"\n\n def _identity_with_no_bootstrap(self) -> dict:\n # Sentinel: bootstrap_variables key is present and explicitly None\n # (the assembler stamps None when VariableText__c is NOT_SET).\n return {\"bootstrap_variables\": None}\n\n def test_messaging_channel_keeps_messaging_path_addendum(self):\n out = \"\\n\".join(render_dc._section_session_bootstrap(\n self._identity_with_no_bootstrap(),\n channel=\"SCRT2 - EmbeddedMessaging\",\n ))\n self.assertIn(\"no bootstrap variables\", out)\n self.assertIn(\"production messaging path\", out)\n # Alias.\n out2 = \"\\n\".join(render_dc._section_session_bootstrap(\n self._identity_with_no_bootstrap(),\n channel=\"Messaging\",\n ))\n self.assertIn(\"production messaging path\", out2)\n\n def test_non_messaging_channel_omits_messaging_path(self):\n # E&O is the wheelz that surfaced this bug — Atlas-planner\n # session on the \"E & O\" channel was being mislabeled\n # \"production messaging path\".\n out = \"\\n\".join(render_dc._section_session_bootstrap(\n self._identity_with_no_bootstrap(),\n channel=\"E & O\",\n ))\n self.assertIn(\"no bootstrap variables\", out)\n self.assertNotIn(\"production messaging path\", out)\n self.assertNotIn(\"messaging\", out.lower())\n\n def test_null_channel_uses_neutral_wording(self):\n # No channel info at all — must not assume messaging.\n out = \"\\n\".join(render_dc._section_session_bootstrap(\n self._identity_with_no_bootstrap(),\n channel=None,\n ))\n self.assertIn(\"no bootstrap variables\", out)\n self.assertNotIn(\"production messaging path\", out)\n\n def test_default_channel_kwarg_is_neutral(self):\n # Backward-compat: callers that don't pass channel get the\n # neutral wording, never the messaging-path label.\n out = \"\\n\".join(render_dc._section_session_bootstrap(\n self._identity_with_no_bootstrap(),\n ))\n self.assertIn(\"no bootstrap variables\", out)\n self.assertNotIn(\"production messaging path\", out)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10758,"content_sha256":"7e6dd55e2833c5ce794299aecbd110dea91834166ec110a652bd7dc9dc8e5f3c"},{"filename":"scripts/tests/test_render_dc_integration.py","content":"\"\"\"End-to-end integration tests for ``render_dc.render``.\n\nDrives the full renderer against the synthetic session fixture (assembled\nthrough ``assemble_dc.assemble`` first). Asserts on section presence,\nsubstring content of identity / transcript / hierarchical-trace / id-ref\nsections, and graceful degradation paths (no session_dir, gateway-direct\nshape, minimal tree).\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport unittest\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport assemble_dc # type: ignore\nimport render_dc # type: ignore\nfrom config import paths # type: ignore\nfrom .fixtures.synthetic_session import ( # type: ignore\n IDS, write_to_disk,\n)\n\n\nclass _RenderHarness:\n \"\"\"Materialize fixture, assemble, render.\"\"\"\n\n def __init__(self, *, with_session_dir: bool = True,\n mutate_files=None):\n self.with_session_dir = with_session_dir\n self.mutate_files = mutate_files\n\n def __enter__(self):\n self._tmp = TemporaryDirectory()\n self._tmpdir = Path(self._tmp.name)\n sdir = write_to_disk(self._tmpdir)\n if self.mutate_files:\n self.mutate_files(sdir)\n self._patch = mock.patch.object(paths, \"DATA_ROOT\", self._tmpdir)\n self._patch.start()\n self.tree, self.sdir = assemble_dc.assemble(IDS.SID)\n self.manifest = json.loads(\n (self.sdir / \"dc._session_manifest.json\").read_text()\n )\n self.md = render_dc.render(\n self.tree,\n manifest=self.manifest,\n session_dir=self.sdir if self.with_session_dir else None,\n )\n return self\n\n def __exit__(self, *exc):\n self._patch.stop()\n self._tmp.cleanup()\n\n\n# -----------------------------------------------------------------------------\n# Top-level shape — sections present\n# -----------------------------------------------------------------------------\n\n\nclass RenderSectionsPresentTests(unittest.TestCase):\n\n def test_renders_session_identity_section(self):\n with _RenderHarness() as h:\n self.assertIn(\"## Session identity\", h.md)\n\n def test_renders_id_reference_section(self):\n with _RenderHarness() as h:\n self.assertIn(\"## ID reference\", h.md)\n\n def test_renders_hierarchical_trace_section(self):\n with _RenderHarness() as h:\n self.assertIn(\"## Complete hierarchical trace\", h.md)\n\n def test_renders_per_turn_summary_section(self):\n with _RenderHarness() as h:\n self.assertIn(\"## Per-turn summary\", h.md)\n\n def test_renders_transcript_section(self):\n with _RenderHarness() as h:\n self.assertIn(\"## Transcript\", h.md)\n\n\n# -----------------------------------------------------------------------------\n# Identity content\n# -----------------------------------------------------------------------------\n\n\nclass RenderIdentityTests(unittest.TestCase):\n\n def test_identity_shows_session_id(self):\n with _RenderHarness() as h:\n self.assertIn(IDS.SID, h.md)\n\n def test_identity_shows_messaging_session_id(self):\n with _RenderHarness() as h:\n self.assertIn(\"0MwTESTMSG12345AAA\", h.md)\n\n def test_identity_shows_total_duration_in_human_form(self):\n with _RenderHarness() as h:\n # Session is 90 seconds → \"1m 30.000s\"\n self.assertIn(\"1m 30.000s\", h.md)\n\n def test_identity_shows_channel_and_end_type(self):\n with _RenderHarness() as h:\n self.assertIn(\"SCRT2 - EmbeddedMessaging\", h.md)\n self.assertIn(\"USER_ENDED\", h.md)\n\n def test_identity_marks_session_end_as_materialized(self):\n with _RenderHarness() as h:\n self.assertIn(\"✓ materialized\", h.md)\n\n\n# -----------------------------------------------------------------------------\n# ID reference\n# -----------------------------------------------------------------------------\n\n\nclass RenderIdReferenceTests(unittest.TestCase):\n\n def test_id_ref_lists_all_three_interactions(self):\n with _RenderHarness() as h:\n self.assertIn(IDS.IXN_START, h.md)\n self.assertIn(IDS.IXN_TURN, h.md)\n self.assertIn(IDS.IXN_END, h.md)\n\n def test_id_ref_lists_both_participants_with_roles(self):\n with _RenderHarness() as h:\n self.assertIn(IDS.PART_AGENT, h.md)\n self.assertIn(IDS.PART_USER, h.md)\n self.assertIn(\"role=AGENT\", h.md)\n self.assertIn(\"role=USER\", h.md)\n\n def test_id_ref_lists_all_three_steps(self):\n with _RenderHarness() as h:\n self.assertIn(\"TopicSelection\", h.md)\n self.assertIn(\"PrimaryAction\", h.md)\n self.assertIn(\"FinalGuard\", h.md)\n\n def test_id_ref_lists_both_gateway_requests_with_binding_method(self):\n with _RenderHarness() as h:\n self.assertIn(IDS.GW_REQ_DECLARED, h.md)\n self.assertIn(IDS.GW_REQ_WINDOW, h.md)\n self.assertIn(\"binding=declared\", h.md)\n self.assertIn(\"binding=timestamp_window\", h.md)\n\n def test_id_ref_includes_extracted_trace_id(self):\n with _RenderHarness() as h:\n # Trace_id was pulled from HTML-escaped AttributeText; once\n # decoded it must appear verbatim in the id-ref section.\n self.assertIn(IDS.TRACE_ID, h.md)\n\n\n# -----------------------------------------------------------------------------\n# Transcript\n# -----------------------------------------------------------------------------\n\n\nclass RenderTranscriptTests(unittest.TestCase):\n\n def test_transcript_includes_user_message(self):\n with _RenderHarness() as h:\n self.assertIn(\"How do I refund my order?\", h.md)\n\n def test_transcript_includes_agent_message(self):\n with _RenderHarness() as h:\n self.assertIn(\"I'll help you with the refund process.\", h.md)\n\n\n# -----------------------------------------------------------------------------\n# Graceful degradation paths\n# -----------------------------------------------------------------------------\n\n\nclass RenderDegradationTests(unittest.TestCase):\n\n def test_renders_without_session_dir(self):\n # No session_dir → no latency-rollup section, but the rest of\n # the render should succeed and contain the standard sections.\n with _RenderHarness(with_session_dir=False) as h:\n self.assertIn(\"## Session identity\", h.md)\n self.assertIn(\"## Complete hierarchical trace\", h.md)\n\n def test_minimal_tree_short_circuits_to_short_markdown(self):\n def empty_sessions(sdir: Path) -> None:\n (sdir / \"dc.sessions.json\").write_text(\"[]\")\n\n with _RenderHarness(mutate_files=empty_sessions) as h:\n # session_not_found minimal tree still produces something\n # readable that contains the SID.\n self.assertIn(IDS.SID, h.md)\n # Doesn't produce the full 9-section layout — verify by\n # absence of a section that requires interactions.\n self.assertNotIn(\"## Complete hierarchical trace\", h.md)\n\n\n# -----------------------------------------------------------------------------\n# Output is non-empty markdown\n# -----------------------------------------------------------------------------\n\n\nclass RenderOutputBasicsTests(unittest.TestCase):\n\n def test_output_starts_with_h1_header(self):\n with _RenderHarness() as h:\n first_line = h.md.splitlines()[0]\n self.assertTrue(first_line.startswith(\"# Session \"))\n\n def test_output_is_substantial(self):\n # A real session produces several KB; a hand-built fixture with\n # 1 turn + 3 steps + 2 gateway calls should produce ≥ 2 KB.\n with _RenderHarness() as h:\n self.assertGreater(len(h.md), 2000)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":7935,"content_sha256":"7f8360eb3b232b46d2e849261e3df16aa17ef36114b8130d83665787484b9e49"},{"filename":"scripts/tests/test_render_dc_planner_llm_calls.py","content":"\"\"\"Tests for the opt-in Planner LLM calls section in render_dc.\n\nThe full-tree branch of `render_dc.render()` by default surfaces only a\none-line `decoded:` summary on each generation. The `--show-prompts` flag\n(CLI) / `show_prompts=True` (programmatic) opts in to a section that\nemits the full prompt + response per LLM call.\n\nCoverage:\n - assemble_dc: hierarchical-tree gw view carries `prompt_text` + `prompt_template_dev_name`\n - render_dc: section omitted by default\n - render_dc: section emitted with show_prompts=True\n - render_dc: section walks all interactions[].steps[] and renders one block per gateway_request\n - render_dc: 64 KB byte-cap on prompt display still applies (regression guard)\n - render_dc: empty/missing prompt → renders `(empty)` not crash\n - render_dc: response_text is HTML-unescaped before display\n - render_dc: gateway-direct branch unchanged (still emits Per-call detail)\n\nTest isolation: synthesizes a session tree in memory (no on-disk DC fetch),\ncalls `render(...)` directly, and asserts on the produced markdown.\n\"\"\"\nfrom __future__ import annotations\n\nimport unittest\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport assemble_dc # type: ignore\nimport render_dc # type: ignore\n\n\n# ---------------------------------------------------------------------------\n# Test fixtures: minimal full-tree shape that render() will accept\n# ---------------------------------------------------------------------------\n\ndef _full_tree(*, with_prompt_text: bool = True,\n with_response_text: bool = True,\n num_calls: int = 2) -> dict:\n \"\"\"Build a synthetic full-tree dict with `num_calls` gateway requests.\"\"\"\n steps = []\n for i in range(num_calls):\n gw = {\n \"gateway_request_id\": f\"gw-id-{i}\",\n \"model\": \"gpt-4.1-2025-04-14\",\n \"provider\": \"salesforce\",\n \"prompt_template_dev_name\": \"Atlas__AgentGraphReasoningPrompt\",\n \"prompt_tokens\": 100 + i,\n \"completion_tokens\": 50,\n \"total_tokens\": 150 + i,\n \"prompt_text\": (\n f\"role: user\\nMessage {i}\\nrole: assistant\\nResponse {i}\"\n if with_prompt_text else None\n ),\n \"response\": {\"finish_reason\": \"stop\"},\n }\n gen = {\n \"generation_id\": f\"gen-{i}\",\n \"response_text\": (\n f'{{\"content\":\"\",\"toolInvocations\":[{{\"id\":\"call_{i}\",'\n f'\"function\":{{\"name\":\"some_tool\",\"arguments\":\"{{}}\"}}}}]}}'\n if with_response_text else None\n ),\n }\n steps.append({\n \"id\": f\"step-{i}\",\n \"type\": \"LLM_STEP\",\n \"gateway_request\": gw,\n \"generation\": gen,\n })\n return {\n \"session\": {\n \"_schema_version\": 1,\n \"id\": \"fixture-session-uuid-0000\",\n \"start_ts\": \"2026-05-14T10:00:00.000Z\",\n \"end_ts\": \"2026-05-14T10:00:30.000Z\",\n \"identity\": {},\n \"interactions\": [{\n \"id\": \"interaction-0000\",\n \"type\": \"TURN\",\n \"steps\": steps,\n }],\n },\n }\n\n\n# ---------------------------------------------------------------------------\n# Assembler: _build_gw_view now carries prompt_text + prompt_template_dev_name\n# ---------------------------------------------------------------------------\n\nclass BuildGwViewCarriesPromptTests(unittest.TestCase):\n \"\"\"Hierarchical view propagates prompt__c so renderer can use it.\"\"\"\n\n def _idx_dispatch(self):\n from assemble_dc import Indexes, PolymorphicSplits\n idx = Indexes(\n interactions_by_id={}, participants_by_id={},\n generations_by_id={}, gw_req_by_id={},\n gw_resp_by_resp_id={}, feedback_by_id={},\n gw_resp_by_req_id={}, steps_by_interaction={},\n messages_by_interaction={}, gw_tags_by_parent={},\n gw_md_by_parent={}, gw_llm_by_parent={},\n quality_by_parent={}, quality_by_id={},\n feedback_by_gen={}, feedback_details_by_parent={},\n participant_role_by_id={},\n )\n dispatch = PolymorphicSplits(\n categories_by_generation={}, categories_by_quality={},\n gw_records_by_gw_req={}, gw_records_by_feedback={},\n tag_assoc_session=[], tag_assoc_by_interaction={},\n tag_assoc_by_moment={},\n )\n return idx, dispatch\n\n def test_prompt_text_propagated(self):\n idx, dispatch = self._idx_dispatch()\n gw_req = {\n \"gatewayRequestId__c\": \"gw-fixture-0000\",\n \"feature__c\": \"plannerservice\",\n \"model__c\": \"gpt-4.1\",\n \"provider__c\": \"salesforce\",\n \"promptTemplateDevName__c\": \"Atlas__AgentGraphReasoningPrompt\",\n \"promptTokens__c\": 874.0,\n \"completionTokens__c\": 1.0,\n \"totalTokens__c\": 875.0,\n \"prompt__c\": \"role: user\\nHello\\nrole: assistant\\nHi\",\n }\n view = assemble_dc._build_gw_view(\n gw_req, \"declared\", idx=idx, dispatch=dispatch,\n )\n self.assertEqual(view[\"prompt_text\"],\n \"role: user\\nHello\\nrole: assistant\\nHi\")\n self.assertEqual(view[\"prompt_template_dev_name\"],\n \"Atlas__AgentGraphReasoningPrompt\")\n\n def test_missing_prompt_propagates_as_none(self):\n \"\"\"No prompt__c on the source row → view.prompt_text is None, no crash.\"\"\"\n idx, dispatch = self._idx_dispatch()\n gw_req = {\"gatewayRequestId__c\": \"gw-no-prompt\"}\n view = assemble_dc._build_gw_view(\n gw_req, \"declared\", idx=idx, dispatch=dispatch,\n )\n self.assertIsNone(view[\"prompt_text\"])\n\n\n# ---------------------------------------------------------------------------\n# Renderer: opt-in section\n# ---------------------------------------------------------------------------\n\nclass RenderShowPromptsOffByDefaultTests(unittest.TestCase):\n\n def test_section_absent_by_default(self):\n tree = _full_tree()\n md = render_dc.render(tree)\n self.assertNotIn(\"Planner LLM calls\", md)\n self.assertNotIn(\"**Prompt** (full input sent to the model)\", md)\n\n def test_section_present_with_flag(self):\n tree = _full_tree()\n md = render_dc.render(tree, show_prompts=True)\n self.assertIn(\"## Planner LLM calls\", md)\n self.assertIn(\"**Prompt**\", md)\n\n\nclass RenderShowPromptsContentsTests(unittest.TestCase):\n\n def test_section_renders_one_block_per_call(self):\n tree = _full_tree(num_calls=3)\n md = render_dc.render(tree, show_prompts=True)\n # Three \"#### LLM call N\" headers — one per gateway_request.\n self.assertEqual(md.count(\"#### LLM call 1 — \"), 1)\n self.assertEqual(md.count(\"#### LLM call 2 — \"), 1)\n self.assertEqual(md.count(\"#### LLM call 3 — \"), 1)\n # Both blocks (prompt + response) per call → 3 of each.\n self.assertEqual(md.count(\"**Prompt**\"), 3)\n self.assertEqual(md.count(\"**Response**\"), 3)\n\n def test_prompt_text_appears_verbatim(self):\n tree = _full_tree(num_calls=1)\n md = render_dc.render(tree, show_prompts=True)\n self.assertIn(\"role: user\\nMessage 0\\nrole: assistant\\nResponse 0\", md)\n\n def test_response_text_html_unescaped(self):\n \"\"\"Generation.response_text may have " from the wire format —\n renderer must HTML-unescape before fenced-block insertion.\"\"\"\n tree = _full_tree(num_calls=1)\n # Replace the response with HTML-escaped content.\n tree[\"session\"][\"interactions\"][0][\"steps\"][0][\"generation\"][\"response_text\"] = (\n '{"content":"Hello"}'\n )\n md = render_dc.render(tree, show_prompts=True)\n self.assertIn('{\"content\":\"Hello\"}', md)\n self.assertNotIn(\""\", md)\n\n def test_empty_prompt_renders_empty_marker_not_crash(self):\n \"\"\"Missing prompt + response → block emits `(empty)` cleanly.\"\"\"\n tree = _full_tree(with_prompt_text=False, with_response_text=False,\n num_calls=1)\n md = render_dc.render(tree, show_prompts=True)\n # Both blocks present, but the body is `(empty)`.\n self.assertIn(\"**Prompt**\", md)\n self.assertIn(\"(empty)\", md)\n\n def test_section_empty_when_no_steps_have_gateway_request(self):\n \"\"\"Steps without gateway_request → no section emitted at all.\"\"\"\n tree = _full_tree(num_calls=1)\n # Strip the gateway_request off the only step.\n tree[\"session\"][\"interactions\"][0][\"steps\"][0].pop(\"gateway_request\")\n md = render_dc.render(tree, show_prompts=True)\n self.assertNotIn(\"## Planner LLM calls\", md)\n\n\nclass RenderPromptDisplayCapTests(unittest.TestCase):\n \"\"\"Regression guard: the 64 KB byte-cap from the gateway-direct path\n still applies on the new full-tree path.\"\"\"\n\n def test_oversize_prompt_truncated(self):\n tree = _full_tree(num_calls=1)\n # 100 KB of ASCII — well over the 64 KB cap.\n oversize = \"x\" * (100 * 1024)\n tree[\"session\"][\"interactions\"][0][\"steps\"][0][\n \"gateway_request\"][\"prompt_text\"] = oversize\n md = render_dc.render(tree, show_prompts=True)\n # Truncation marker present.\n self.assertIn(\"…[truncated; full prompt in dc.gateway_requests.json]\",\n md)\n # Body capped — not the full 100 KB.\n self.assertLess(md.count(\"x\"), 100 * 1024)\n\n\nclass RenderGatewayDirectUnchangedTests(unittest.TestCase):\n \"\"\"The gateway-direct branch (STDM-not-yet-materialized) keeps emitting\n its Per-call detail section header + per-call summary line by default.\n The full prompt body is gated behind ``show_prompts=True`` to match the\n documented contract for ``dc._session_summary.md`` (default summary\n omits prompts; ``--show-prompts`` opts in).\n \"\"\"\n\n def _gateway_direct_tree(self):\n return {\n \"_source\": \"gateway_direct\",\n \"session\": {\n \"_schema_version\": 1,\n \"id\": \"fixture-session-uuid-0000\",\n \"start_ts\": \"2026-05-14T10:00:00.000Z\",\n \"end_ts\": \"2026-05-14T10:00:30.000Z\",\n \"identity\": {},\n \"gateway_chain\": [{\n \"gateway_request_id\": \"gw-direct-0\",\n \"model\": \"gpt-4.1\",\n \"provider\": \"salesforce\",\n \"prompt_template_dev_name\": \"Atlas__X\",\n \"prompt_tokens\": 100,\n \"completion_tokens\": 50,\n \"total_tokens\": 150,\n \"prompt_text\": \"Some prompt\",\n \"response\": {\"finish_reason\": \"stop\"},\n }],\n },\n }\n\n def test_gateway_direct_per_call_section_header_still_emitted(self):\n \"\"\"Default render keeps the Per-call detail header + summary line.\"\"\"\n md = render_dc.render(self._gateway_direct_tree())\n # gateway-direct path's own header is \"## Per-call detail\".\n self.assertIn(\"## Per-call detail\", md)\n # Per-call summary line (model/provider/tokens) still emits.\n self.assertIn(\"model=gpt-4.1\", md)\n # Prompt body suppressed by default (matches doc contract).\n self.assertNotIn(\"Some prompt\", md)\n self.assertNotIn(\"**Prompt**\", md)\n # gateway-direct never has response_text → no Response block.\n self.assertNotIn(\"**Response**\", md)\n\n def test_gateway_direct_per_call_detail_with_show_prompts(self):\n \"\"\"Opt-in render exposes the full prompt body in the per-call block.\"\"\"\n md = render_dc.render(self._gateway_direct_tree(), show_prompts=True)\n self.assertIn(\"## Per-call detail\", md)\n self.assertIn(\"**Prompt**\", md)\n self.assertIn(\"Some prompt\", md)\n # Response block still suppressed — gateway-direct never carries it.\n self.assertNotIn(\"**Response**\", md)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":12086,"content_sha256":"f9e9f5fdef2ffc886661875217a84dfd580db070afc9ccec8ffd89e18e4380e6"},{"filename":"scripts/tests/test_render_dc_show_prompts_gating.py","content":"\"\"\"Default-gating contract for the ``--show-prompts`` flag in render_dc.\n\nBoth render branches — gateway-direct (STDM lag) and full-tree (post-STDM\nmaterialization) — must suppress the full prompt block by default and only\nemit it when the caller opts in via ``show_prompts=True`` (CLI:\n``--show-prompts``).\n\nThis pins the doc contract from ``SKILL.md``:\n\n The default ``dc._session_summary.md`` does NOT include the input\n prompt or the model's full response — only a one-line ``decoded:``\n summary on each generation. When the user asks for the actual prompt\n sent to the model … re-render with the ``--show-prompts`` flag.\n\nA regression on the gateway-direct branch (renderer dropped the\nparameter on the ``--show-prompts`` plumbing path) leaked the full prompt\non every fresh-session render. These tests guard both branches and the\n``_render_call_detail_block`` helper directly so the two flags\n(``show_prompts`` / ``show_response_text``) gate independently.\n\"\"\"\nfrom __future__ import annotations\n\nimport unittest\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport render_dc # type: ignore\n\n\n# Distinct sentinel strings so test failures point at the exact branch\n# that leaked. ``role: user`` is included because the live render uses\n# that exact substring on real prompts and `grep` searches in the\n# investigation runbook key off it.\n_GATEWAY_DIRECT_PROMPT_MARKER = \"ROLE-USER-GATEWAY-DIRECT-MARKER\"\n_FULL_TREE_PROMPT_MARKER = \"ROLE-USER-FULL-TREE-MARKER\"\n_FULL_TREE_RESPONSE_MARKER = \"ROLE-ASSISTANT-FULL-TREE-MARKER\"\n\n\ndef _gateway_direct_tree_with_marker() -> dict:\n \"\"\"Synthetic gateway-direct tree carrying a unique prompt marker.\"\"\"\n return {\n \"_source\": \"gateway_direct\",\n \"session\": {\n \"_schema_version\": 1,\n \"id\": \"fixture-gd-session-0000\",\n \"start_ts\": \"2026-05-26T10:00:00.000Z\",\n \"end_ts\": \"2026-05-26T10:00:30.000Z\",\n \"identity\": {},\n \"gateway_chain\": [{\n \"gateway_request_id\": \"gw-gd-0\",\n \"model\": \"gpt-4.1\",\n \"provider\": \"salesforce\",\n \"prompt_template_dev_name\": \"Atlas__X\",\n \"prompt_tokens\": 100,\n \"completion_tokens\": 50,\n \"total_tokens\": 150,\n \"prompt_text\": _GATEWAY_DIRECT_PROMPT_MARKER,\n \"response\": {\"finish_reason\": \"stop\"},\n }],\n },\n }\n\n\ndef _full_tree_with_marker() -> dict:\n \"\"\"Synthetic full-tree session with one LLM call, unique markers.\"\"\"\n return {\n \"session\": {\n \"_schema_version\": 1,\n \"id\": \"fixture-ft-session-0000\",\n \"start_ts\": \"2026-05-26T10:00:00.000Z\",\n \"end_ts\": \"2026-05-26T10:00:30.000Z\",\n \"identity\": {},\n \"interactions\": [{\n \"id\": \"interaction-0000\",\n \"type\": \"TURN\",\n \"steps\": [{\n \"id\": \"step-0\",\n \"type\": \"LLM_STEP\",\n \"gateway_request\": {\n \"gateway_request_id\": \"gw-ft-0\",\n \"model\": \"gpt-4.1\",\n \"provider\": \"salesforce\",\n \"prompt_template_dev_name\": \"Atlas__X\",\n \"prompt_tokens\": 100,\n \"completion_tokens\": 50,\n \"total_tokens\": 150,\n \"prompt_text\": _FULL_TREE_PROMPT_MARKER,\n \"response\": {\"finish_reason\": \"stop\"},\n },\n \"generation\": {\n \"generation_id\": \"gen-0\",\n \"response_text\": _FULL_TREE_RESPONSE_MARKER,\n },\n }],\n }],\n },\n }\n\n\n# ---------------------------------------------------------------------------\n# Gateway-direct branch (the leaky branch this fix targets)\n# ---------------------------------------------------------------------------\n\n\nclass GatewayDirectBranchGatingTests(unittest.TestCase):\n\n def test_gateway_direct_no_prompt_block_by_default(self):\n \"\"\"Default render: no **Prompt** heading, no prompt body bytes.\"\"\"\n md = render_dc.render(_gateway_direct_tree_with_marker())\n self.assertNotIn(_GATEWAY_DIRECT_PROMPT_MARKER, md)\n self.assertNotIn(\"**Prompt**\", md)\n # The Per-call detail section header itself stays — only the\n # body is gated.\n self.assertIn(\"## Per-call detail\", md)\n\n def test_gateway_direct_emits_prompt_block_when_show_prompts_true(self):\n \"\"\"Opt-in render: prompt body + heading both surface.\"\"\"\n md = render_dc.render(_gateway_direct_tree_with_marker(),\n show_prompts=True)\n self.assertIn(_GATEWAY_DIRECT_PROMPT_MARKER, md)\n self.assertIn(\"**Prompt**\", md)\n\n\n# ---------------------------------------------------------------------------\n# Full-tree branch (regression guard — already correctly gated, lock it in)\n# ---------------------------------------------------------------------------\n\n\nclass FullTreeBranchGatingTests(unittest.TestCase):\n\n def test_full_tree_no_prompt_block_by_default_regression(self):\n \"\"\"Full-tree default: no Planner LLM calls section, no prompt body.\"\"\"\n md = render_dc.render(_full_tree_with_marker())\n self.assertNotIn(_FULL_TREE_PROMPT_MARKER, md)\n self.assertNotIn(\"**Prompt**\", md)\n self.assertNotIn(\"## Planner LLM calls\", md)\n\n def test_full_tree_emits_prompt_block_when_show_prompts_true_regression(self):\n \"\"\"Full-tree opt-in: section header + prompt + response all surface.\"\"\"\n md = render_dc.render(_full_tree_with_marker(), show_prompts=True)\n self.assertIn(\"## Planner LLM calls\", md)\n self.assertIn(\"**Prompt**\", md)\n self.assertIn(_FULL_TREE_PROMPT_MARKER, md)\n self.assertIn(\"**Response**\", md)\n self.assertIn(_FULL_TREE_RESPONSE_MARKER, md)\n\n\n# ---------------------------------------------------------------------------\n# Helper: independent gating of prompt vs response block\n# ---------------------------------------------------------------------------\n\n\nclass RenderCallDetailBlockHelperTests(unittest.TestCase):\n \"\"\"``_render_call_detail_block`` must gate prompt and response\n independently. The gateway-direct path uses\n ``show_prompts=False, show_response_text=False`` by default, while\n the full-tree path post-fix uses\n ``show_prompts=True, show_response_text=True`` once the section\n guard has cleared. Mixed combinations must work too.\"\"\"\n\n def _call(self):\n return {\n \"gateway_request_id\": \"gw-helper-0\",\n \"model\": \"gpt-4.1\",\n \"provider\": \"salesforce\",\n \"prompt_template_dev_name\": \"Atlas__X\",\n \"prompt_tokens\": 100,\n \"completion_tokens\": 50,\n \"total_tokens\": 150,\n \"prompt_text\": \"PROMPT-MARKER\",\n \"response\": {\"finish_reason\": \"stop\"},\n \"response_text\": \"RESPONSE-MARKER\",\n }\n\n def test_response_only(self):\n \"\"\"show_prompts=False + show_response_text=True → response only.\"\"\"\n block = \"\\n\".join(render_dc._render_call_detail_block(\n self._call(), 1, show_prompts=False, show_response_text=True))\n self.assertNotIn(\"**Prompt**\", block)\n self.assertNotIn(\"PROMPT-MARKER\", block)\n self.assertIn(\"**Response**\", block)\n self.assertIn(\"RESPONSE-MARKER\", block)\n\n def test_prompt_only(self):\n \"\"\"show_prompts=True + show_response_text=False → prompt only.\"\"\"\n block = \"\\n\".join(render_dc._render_call_detail_block(\n self._call(), 1, show_prompts=True, show_response_text=False))\n self.assertIn(\"**Prompt**\", block)\n self.assertIn(\"PROMPT-MARKER\", block)\n self.assertNotIn(\"**Response**\", block)\n self.assertNotIn(\"RESPONSE-MARKER\", block)\n\n def test_neither(self):\n \"\"\"Both flags off → summary line only, no fenced blocks.\"\"\"\n block = \"\\n\".join(render_dc._render_call_detail_block(\n self._call(), 1))\n self.assertNotIn(\"**Prompt**\", block)\n self.assertNotIn(\"**Response**\", block)\n self.assertNotIn(\"PROMPT-MARKER\", block)\n self.assertNotIn(\"RESPONSE-MARKER\", block)\n # Summary line still emits.\n self.assertIn(\"model=gpt-4.1\", block)\n\n def test_both(self):\n \"\"\"Both flags on → both blocks present.\"\"\"\n block = \"\\n\".join(render_dc._render_call_detail_block(\n self._call(), 1, show_prompts=True, show_response_text=True))\n self.assertIn(\"**Prompt**\", block)\n self.assertIn(\"PROMPT-MARKER\", block)\n self.assertIn(\"**Response**\", block)\n self.assertIn(\"RESPONSE-MARKER\", block)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8879,"content_sha256":"fb1b84af2f7b550df498a9397e2e73628b9e5ce36692606f6dbf43fc7ad77ab7"},{"filename":"scripts/tests/test_resolve_from_disk.py","content":"\"\"\"Tests for ``resolve_session.resolve_from_disk``.\n\nThe function walks ``DATA_ROOT`` for ``dc.sessions.json`` files and\nreturns the AI-agent session UUID whose row has a matching\n``ssot__RelatedMessagingSessionId__c``. Must work across:\n\n - the nested layout (``\u003corg>/\u003cagent>__\u003cver>/\u003cuuid>/``),\n - historic flat layout (``\u003cuuid>/``),\n - user-created archive suffix dirs (skip — stale duplicate rows),\n - distinct-UUID multi-match (SystemExit),\n - UUID pass-through (no-op).\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport tempfile\nimport unittest\nfrom pathlib import Path\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport resolve_session # type: ignore\n\n\n_UUID_A = \"019dface-0000-7000-8000-000000000001\"\n_UUID_B = \"019dface-0000-7000-8000-000000000002\"\n_MSG_ID = \"0MwTESTMSG67890BBB\"\n\n\ndef _write_sessions_row(session_dir: Path, uuid: str, msg_id: str) -> None:\n session_dir.mkdir(parents=True, exist_ok=True)\n (session_dir / \"dc.sessions.json\").write_text(json.dumps([{\n \"ssot__Id__c\": uuid,\n \"ssot__RelatedMessagingSessionId__c\": msg_id,\n }]))\n\n\nclass ResolveFromDiskTests(unittest.TestCase):\n\n def test_nested_layout_finds_uuid(self):\n \"\"\"``\u003corg>/\u003cagent>/\u003cuuid>/dc.sessions.json`` — the current layout\n produced by ``storage.save``. Must find the messaging id.\"\"\"\n with tempfile.TemporaryDirectory() as tmp:\n root = Path(tmp)\n _write_sessions_row(\n root / \"00D000000000000\" / \"DemoAgent__v5\" / _UUID_A,\n _UUID_A, _MSG_ID,\n )\n with mock.patch.object(resolve_session, \"DATA_ROOT\", root):\n self.assertEqual(resolve_session.resolve_from_disk(_MSG_ID), _UUID_A)\n\n def test_flat_layout_still_finds_uuid(self):\n \"\"\"Historic ``\u003cuuid>/dc.sessions.json`` layout — same rglob walk\n catches it. Regression guard for backwards compatibility.\"\"\"\n with tempfile.TemporaryDirectory() as tmp:\n root = Path(tmp)\n _write_sessions_row(root / _UUID_A, _UUID_A, _MSG_ID)\n with mock.patch.object(resolve_session, \"DATA_ROOT\", root):\n self.assertEqual(resolve_session.resolve_from_disk(_MSG_ID), _UUID_A)\n\n def test_archive_suffix_dirs_are_skipped(self):\n \"\"\"Primary ``\u003cuuid>/`` + duplicate ``\u003cuuid> - archive 1/`` hold\n identical rows. The archive copy must not trigger a multi-match.\"\"\"\n with tempfile.TemporaryDirectory() as tmp:\n root = Path(tmp)\n org_agent = root / \"00D000000000000\" / \"DemoAgent__v5\"\n _write_sessions_row(org_agent / _UUID_A, _UUID_A, _MSG_ID)\n _write_sessions_row(\n org_agent / f\"{_UUID_A} - archive 1\", _UUID_A, _MSG_ID,\n )\n with mock.patch.object(resolve_session, \"DATA_ROOT\", root):\n self.assertEqual(resolve_session.resolve_from_disk(_MSG_ID), _UUID_A)\n\n def test_distinct_uuids_with_same_msg_id_raise(self):\n \"\"\"Two DISTINCT uuid dirs carrying the same messaging id is the\n real ambiguity case — SystemExit with both uuids listed.\"\"\"\n with tempfile.TemporaryDirectory() as tmp:\n root = Path(tmp)\n org_agent = root / \"00D000000000000\" / \"DemoAgent__v5\"\n _write_sessions_row(org_agent / _UUID_A, _UUID_A, _MSG_ID)\n _write_sessions_row(org_agent / _UUID_B, _UUID_B, _MSG_ID)\n with mock.patch.object(resolve_session, \"DATA_ROOT\", root):\n with self.assertRaises(SystemExit) as cm:\n resolve_session.resolve_from_disk(_MSG_ID)\n msg = str(cm.exception)\n self.assertIn(_UUID_A, msg)\n self.assertIn(_UUID_B, msg)\n self.assertIn(\"matches 2\", msg)\n\n def test_uuid_input_passes_through(self):\n \"\"\"UUID input is returned as-is without scanning. The caller\n validates disk presence downstream.\"\"\"\n with tempfile.TemporaryDirectory() as tmp:\n with mock.patch.object(resolve_session, \"DATA_ROOT\", Path(tmp)):\n self.assertEqual(resolve_session.resolve_from_disk(_UUID_A), _UUID_A)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4241,"content_sha256":"246b5bf56fd7c02ba9ba8112144b4f93bf350783226dbcfebdb3d8f1700382bd"},{"filename":"scripts/tests/test_resolve_session_main.py","content":"\"\"\"Tests for ``resolve_session.main`` CLI.\n\nArgparse-driven — exercises every branch:\n- UUID pass-through\n- --disk-only hit / miss\n- live path with --org\n- disk-first fallback when no --org\n- not-found-no-org error (no sf default target org either)\n\"\"\"\nfrom __future__ import annotations\n\nimport io\nimport unittest\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom unittest import mock\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport resolve_session # type: ignore\nfrom .fixtures.synthetic_session import IDS, write_to_disk # type: ignore\n\n\nclass _MainHarness:\n \"\"\"Patch sys.argv + stdout/stderr; optionally plant the synthetic\n fixture under DATA_ROOT for disk-resolution tests.\"\"\"\n\n def __init__(self, *, argv: list[str], plant_fixture: bool = False):\n self.argv = argv\n self.plant_fixture = plant_fixture\n\n def __enter__(self):\n self._tmp = TemporaryDirectory()\n self._tmpdir = Path(self._tmp.name)\n if self.plant_fixture:\n write_to_disk(self._tmpdir)\n self._patches = [\n mock.patch.object(resolve_session.sys, \"argv\",\n [\"resolve_session.py\", *self.argv]),\n mock.patch.object(resolve_session, \"DATA_ROOT\", self._tmpdir),\n ]\n for p in self._patches:\n p.start()\n self.stdout = io.StringIO()\n self.stderr = io.StringIO()\n self._stdout_patch = mock.patch(\"sys.stdout\", self.stdout)\n self._stderr_patch = mock.patch(\"sys.stderr\", self.stderr)\n self._stdout_patch.start()\n self._stderr_patch.start()\n return self\n\n def __exit__(self, *exc):\n self._stderr_patch.stop()\n self._stdout_patch.stop()\n for p in self._patches:\n p.stop()\n self._tmp.cleanup()\n\n\n# -----------------------------------------------------------------------------\n# UUID pass-through\n# -----------------------------------------------------------------------------\n\n\nclass UuidPassThroughTests(unittest.TestCase):\n\n def test_uuid_input_prints_unchanged_returns_zero(self):\n with _MainHarness(argv=[\"--id\", IDS.SID]) as h:\n rc = resolve_session.main()\n self.assertEqual(rc, 0)\n self.assertIn(IDS.SID, h.stdout.getvalue())\n\n\n# -----------------------------------------------------------------------------\n# --disk-only branch\n# -----------------------------------------------------------------------------\n\n\nclass DiskOnlyBranchTests(unittest.TestCase):\n\n def test_disk_only_hit_prints_uuid(self):\n with _MainHarness(\n argv=[\"--id\", \"0MwTESTMSG12345AAA\", \"--disk-only\"],\n plant_fixture=True,\n ) as h:\n rc = resolve_session.main()\n self.assertEqual(rc, 0)\n self.assertIn(IDS.SID, h.stdout.getvalue())\n\n def test_disk_only_miss_returns_one(self):\n with _MainHarness(\n argv=[\"--id\", \"0Mw000000000000\", \"--disk-only\"],\n plant_fixture=True,\n ) as h:\n rc = resolve_session.main()\n self.assertEqual(rc, 1)\n self.assertIn(\"no local session dir matches\", h.stderr.getvalue())\n\n\n# -----------------------------------------------------------------------------\n# Live path with --org\n# -----------------------------------------------------------------------------\n\n\nclass LiveOrgPathTests(unittest.TestCase):\n\n def test_live_path_returns_uuid_from_resolver(self):\n with _MainHarness(\n argv=[\"--id\", \"0MwTESTMSG12345AAA\", \"--org\", \"my-org\"],\n ) as h:\n with mock.patch.object(\n resolve_session, \"resolve\", return_value=\"resolved-uuid\",\n ) as r:\n rc = resolve_session.main()\n r.assert_called_once_with(\"0MwTESTMSG12345AAA\", org=\"my-org\")\n self.assertEqual(rc, 0)\n self.assertIn(\"resolved-uuid\", h.stdout.getvalue())\n\n\n# -----------------------------------------------------------------------------\n# Disk-first convenience fallback\n# -----------------------------------------------------------------------------\n\n\nclass DiskFirstFallbackTests(unittest.TestCase):\n\n def test_disk_hit_when_no_org_supplied(self):\n # No --org but disk resolution succeeds via fixture.\n with _MainHarness(\n argv=[\"--id\", \"0MwTESTMSG12345AAA\"],\n plant_fixture=True,\n ) as h:\n rc = resolve_session.main()\n self.assertEqual(rc, 0)\n self.assertIn(IDS.SID, h.stdout.getvalue())\n\n def test_disk_miss_no_org_returns_two_with_hint(self):\n # No --org, disk miss, AND no sf default target org → exit 2 +\n # helpful stderr.\n with _MainHarness(\n argv=[\"--id\", \"0Mw000000000000\"],\n ) as h:\n with mock.patch.object(\n resolve_session, \"default_target_org\",\n side_effect=SystemExit(\"no --org given and no default target org set\"),\n ):\n rc = resolve_session.main()\n self.assertEqual(rc, 2)\n self.assertIn(\"not found on disk\", h.stderr.getvalue())\n self.assertIn(\"--org \u003calias> required\", h.stderr.getvalue())\n self.assertIn(\"no default target org set\", h.stderr.getvalue())\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5287,"content_sha256":"5bdabeabc8417e02615be736dff64bf1f6e49c2da514460f2104ba50a3d5a1bf"},{"filename":"scripts/tests/test_runtime_override.py","content":"\"\"\"Tests for the --data-dir / --cache-dir override mechanism in d360.\n\nD360 has 5 entry scripts (``fetch_dc.py``, ``assemble_dc.py``, ``render_dc.py``,\n``resolve_session.py``, ``discover_sessions.py``) — each invokes the shared\n``_shared.cli_override.apply_overrides()`` helper. Three things must be true\nafter ``--data-dir \u003cpath>`` is parsed:\n\n1. ``paths.DATA_ROOT`` points at the override.\n2. ``config.DATA_ROOT`` points at the override (re-export).\n3. The entry script's local ``DATA_ROOT`` (captured via\n ``from config import DATA_ROOT``) points at the override.\n\nWithout all three, callers reading ``DATA_ROOT`` from the entry script see\nstale defaults.\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nfrom pathlib import Path\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nimport config # type: ignore\nfrom _shared import paths, runtime # type: ignore\nfrom _shared.cli_override import add_cli_flags, apply_overrides # type: ignore\n\n\ndef _reset_overrides() -> None:\n \"\"\"Clear runtime overrides so the test starts from a known state.\"\"\"\n runtime.set_data_root_override(None)\n runtime.set_cache_root_override(None)\n default_data = runtime.resolve_data_root(\"investigating-agentforce-d360\")\n default_cache = runtime.resolve_cache_root(\"investigating-agentforce-d360\")\n paths.DATA_ROOT = default_data\n paths.CACHE_ROOT = default_cache\n config.DATA_ROOT = default_data\n\n\ndef test_default_paths_under_vibe() -> None:\n \"\"\"No overrides → DATA_ROOT points at ~/.vibe/data/investigating-agentforce-d360.\"\"\"\n _reset_overrides()\n expected = Path.home() / \".vibe\" / \"data\" / \"investigating-agentforce-d360\"\n assert paths.DATA_ROOT == expected\n assert config.DATA_ROOT == expected\n\n\ndef test_data_dir_override_rebinds_paths_and_config(tmp_path: Path) -> None:\n \"\"\"--data-dir /tmp/x → paths.DATA_ROOT and config.DATA_ROOT update.\"\"\"\n _reset_overrides()\n override = tmp_path / \"custom-data\"\n ap = argparse.ArgumentParser()\n add_cli_flags(ap)\n args = ap.parse_args([\"--data-dir\", str(override)])\n # Simulate what an entry script does — pass its own globals().\n fake_caller_globals: dict = {\"DATA_ROOT\": paths.DATA_ROOT}\n apply_overrides(args, caller_globals=fake_caller_globals)\n\n assert paths.DATA_ROOT == override, \"paths.DATA_ROOT not rebound\"\n assert config.DATA_ROOT == override, \"config.DATA_ROOT not rebound\"\n assert fake_caller_globals[\"DATA_ROOT\"] == override, \"caller's local DATA_ROOT not rebound\"\n _reset_overrides()\n\n\ndef test_cache_dir_override_rebinds_paths(tmp_path: Path) -> None:\n \"\"\"--cache-dir /tmp/y → paths.CACHE_ROOT updates.\"\"\"\n _reset_overrides()\n override = tmp_path / \"custom-cache\"\n ap = argparse.ArgumentParser()\n add_cli_flags(ap)\n args = ap.parse_args([\"--cache-dir\", str(override)])\n fake_caller_globals: dict = {}\n apply_overrides(args, caller_globals=fake_caller_globals)\n assert paths.CACHE_ROOT == override\n _reset_overrides()\n\n\ndef test_no_override_leaves_defaults_intact() -> None:\n \"\"\"No flags → no mutation.\"\"\"\n _reset_overrides()\n expected = Path.home() / \".vibe\" / \"data\" / \"investigating-agentforce-d360\"\n ap = argparse.ArgumentParser()\n add_cli_flags(ap)\n args = ap.parse_args([])\n fake_caller_globals: dict = {\"DATA_ROOT\": paths.DATA_ROOT}\n apply_overrides(args, caller_globals=fake_caller_globals)\n assert paths.DATA_ROOT == expected\n assert config.DATA_ROOT == expected\n assert fake_caller_globals[\"DATA_ROOT\"] == expected\n\n\ndef test_caller_globals_without_data_root_key_is_safe(tmp_path: Path) -> None:\n \"\"\"Entry scripts that don't capture DATA_ROOT (e.g. discover_sessions.py)\n must not crash when apply_overrides is called with their globals.\"\"\"\n _reset_overrides()\n override = tmp_path / \"custom-data\"\n ap = argparse.ArgumentParser()\n add_cli_flags(ap)\n args = ap.parse_args([\"--data-dir\", str(override)])\n # Empty dict — simulates discover_sessions which doesn't bind DATA_ROOT.\n fake_caller_globals: dict = {}\n apply_overrides(args, caller_globals=fake_caller_globals) # must not raise\n assert paths.DATA_ROOT == override\n assert \"DATA_ROOT\" not in fake_caller_globals # didn't add it\n _reset_overrides()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4268,"content_sha256":"efb51e0f5ee9311a8e2b217f2935bacd98a1a8661c780c1fdfdb661f1591e5c9"},{"filename":"scripts/tests/test_session_shape_dropped_by_stdm.py","content":"\"\"\"Tests for the `gateway_requests_dropped_by_stdm` session_shape enum value.\n\nThis is the case where LLM_STEPs ran but neither gateway_requests nor\ngenerations wrote (frequently observed on Atlas-routed sessions where\nthe STDM exporter dropped writes).\n\"\"\"\nfrom __future__ import annotations\n\nimport unittest\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nfrom fetch_dc import _classify_session_shape # type: ignore\n\n\nclass ClassifySessionShapeTests(unittest.TestCase):\n \"\"\"All 6 shapes plus disambiguation between the two zero-gw shapes.\"\"\"\n\n def test_session_not_found_when_zero_sessions(self):\n out = _classify_session_shape(\n sessions_count=0, steps_total=0, llm_step_count=0,\n steps_with_generation_id=0, gw_req_count=0,\n )\n self.assertEqual(out, \"session_not_found\")\n\n def test_interactions_not_materialized_when_gw_present_steps_empty(self):\n out = _classify_session_shape(\n sessions_count=1, steps_total=0, llm_step_count=0,\n steps_with_generation_id=0, gw_req_count=5,\n )\n self.assertEqual(out, \"interactions_not_materialized_yet\")\n\n def test_abandoned_before_llm_when_steps_present_no_llm(self):\n out = _classify_session_shape(\n sessions_count=1, steps_total=3, llm_step_count=0,\n steps_with_generation_id=0, gw_req_count=0,\n )\n self.assertEqual(out, \"abandoned_before_llm\")\n\n def test_gateway_requests_dropped_by_stdm_when_llm_steps_no_gens_no_gw(self):\n \"\"\"LLM_STEPs ran, but neither generations nor gateway_requests\n wrote. STDM exporter dropped both — frequently observed on\n Atlas-routed sessions.\"\"\"\n out = _classify_session_shape(\n sessions_count=1, steps_total=10, llm_step_count=4,\n steps_with_generation_id=0, gw_req_count=0,\n )\n self.assertEqual(out, \"gateway_requests_dropped_by_stdm\")\n\n def test_planner_ran_no_gateway_logs_when_gens_present_gw_absent(self):\n \"\"\"Distinct from the new STDM-drop shape: generations DID write,\n gateway_requests didn't. Narrower defect.\"\"\"\n out = _classify_session_shape(\n sessions_count=1, steps_total=10, llm_step_count=4,\n steps_with_generation_id=4, gw_req_count=0,\n )\n self.assertEqual(out, \"planner_ran_no_gateway_logs\")\n\n def test_complete_when_everything_present(self):\n out = _classify_session_shape(\n sessions_count=1, steps_total=10, llm_step_count=4,\n steps_with_generation_id=4, gw_req_count=4,\n )\n self.assertEqual(out, \"complete\")\n\n def test_new_shape_takes_priority_over_planner_ran_no_gateway_logs(self):\n \"\"\"When steps_with_generation_id == 0 (and other guards align),\n the new gateway_requests_dropped_by_stdm shape catches the case\n before planner_ran_no_gateway_logs's stricter check.\"\"\"\n out_drop = _classify_session_shape(\n sessions_count=1, steps_total=10, llm_step_count=4,\n steps_with_generation_id=0, gw_req_count=0,\n )\n self.assertEqual(out_drop, \"gateway_requests_dropped_by_stdm\")\n # Single-row diff — set steps_with_generation_id > 0 — picks the\n # other shape, confirming the two are distinguishable.\n out_logs = _classify_session_shape(\n sessions_count=1, steps_total=10, llm_step_count=4,\n steps_with_generation_id=4, gw_req_count=0,\n )\n self.assertEqual(out_logs, \"planner_ran_no_gateway_logs\")\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3593,"content_sha256":"e73bac97022d21e0003224b36fc69d139739193b12f0948d5c5f7c145f6c10ce"},{"filename":"scripts/tests/test_session_shape.py","content":"\"\"\"Tests for ``fetch_dc._classify_session_shape``.\n\nCovers all 5 shapes with a parameterized table:\n\n - session_not_found — sessions.json returned 0 rows\n - interactions_not_materialized_yet — gw_reqs > 0 AND steps == 0 (STDM lag)\n - abandoned_before_llm — steps > 0, LLM_STEP == 0, gw_reqs == 0\n - planner_ran_no_gateway_logs — LLM_STEP > 0 with generation ids, gw_reqs == 0\n - complete — the happy path\n\nOrder matters — the gateway-direct rule sits BEFORE abandoned_before_llm\nbecause ``gw_req_count > 0`` is a stronger positive signal than\n``steps_total > 0``. Verified here by including a case with both signals\ndisjointly (steps==0 on the gateway-direct path).\n\"\"\"\nfrom __future__ import annotations\n\nimport unittest\n\nfrom . import _bootstrap # noqa: F401 — sys.path setup\n\nfrom fetch_dc import _classify_session_shape # type: ignore\n\n\n_CASES = [\n # (label, kwargs, expected)\n (\n \"session_not_found when sessions.json is empty\",\n dict(sessions_count=0, steps_total=0, llm_step_count=0,\n steps_with_generation_id=0, gw_req_count=0),\n \"session_not_found\",\n ),\n (\n \"session_not_found wins even when gw_reqs > 0 (sessions gate runs first)\",\n dict(sessions_count=0, steps_total=0, llm_step_count=0,\n steps_with_generation_id=0, gw_req_count=5),\n \"session_not_found\",\n ),\n (\n \"interactions_not_materialized_yet — fresh session, gateway populated, STDM lagging\",\n dict(sessions_count=1, steps_total=0, llm_step_count=0,\n steps_with_generation_id=0, gw_req_count=3),\n \"interactions_not_materialized_yet\",\n ),\n (\n \"abandoned_before_llm — steps created but no LLM step, no gateway calls\",\n dict(sessions_count=1, steps_total=2, llm_step_count=0,\n steps_with_generation_id=0, gw_req_count=0),\n \"abandoned_before_llm\",\n ),\n (\n \"planner_ran_no_gateway_logs — LLM steps + gen ids but gateway empty\",\n dict(sessions_count=1, steps_total=3, llm_step_count=2,\n steps_with_generation_id=2, gw_req_count=0),\n \"planner_ran_no_gateway_logs\",\n ),\n (\n \"complete — the normal bucket\",\n dict(sessions_count=1, steps_total=5, llm_step_count=3,\n steps_with_generation_id=3, gw_req_count=4),\n \"complete\",\n ),\n]\n\n\nclass ClassifySessionShapeTests(unittest.TestCase):\n \"\"\"Parametric truth-table for the 5-way enum.\"\"\"\n\n def test_all_shapes(self):\n for label, kwargs, expected in _CASES:\n with self.subTest(label=label):\n self.assertEqual(_classify_session_shape(**kwargs), expected)\n\n def test_gateway_direct_precedes_abandoned(self):\n \"\"\"Regression guard: the new rule must fire before abandoned_before_llm.\n\n If someone reorders the checks, a session with gw_reqs > 0 AND\n steps > 0 AND LLM_STEP == 0 (edge case — happens when Step rows\n land while Interaction parent rows are still lagging) could fall\n through incorrectly. Today the rules' inputs are disjoint\n (gateway-direct needs steps==0), so the guard case uses steps==0\n to exercise the ordering directly.\n \"\"\"\n shape = _classify_session_shape(\n sessions_count=1,\n steps_total=0,\n llm_step_count=0,\n steps_with_generation_id=0,\n gw_req_count=1,\n )\n self.assertEqual(shape, \"interactions_not_materialized_yet\")\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3596,"content_sha256":"9828df35eca6ac6dd3d3cc3c8e79b63f74358b9662e1515813739ac6b0b38606"},{"filename":"scripts/tests/test_skill_md_prefix_block.py","content":"\"\"\"Run SKILL.md's \"## Resolving the script prefix\" bash block under bash and zsh.\n\nThe block is the contract the agent executes verbatim, so these tests extract\nit from SKILL.md (no copy to drift) and run it under ``env -i`` against fake\ninstall layouts, then call ``python3 \"$prefix/fetch_dc.py\"`` the way every\nlater SKILL.md invocation does. The stub fetch_dc.py records its own path so\nthe test can assert which copy actually ran.\n\nScenarios: scripts at the candidate root (shipped layout), scripts only under\n``\u003ccandidate>/artifacts/scripts/`` (ADK eval staging layout) via an exported\nSKILL_ROOT and via the install-root probe, root preferred over its own\nartifacts/, candidate order preserved, and nothing found (guard message).\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport re\nimport shutil\nimport subprocess\nimport sys\nimport tempfile\nimport unittest\nfrom pathlib import Path\n\n_SKILL_NAME = \"agentforce-d360-analyze\"\n\n\ndef _find_skill_md() -> Path:\n # contributed-skills: \u003cskill>/scripts/tests/ -> \u003cskill>/SKILL.md\n # outputs/unified-sors: \u003cskill>/artifacts/scripts/tests/ -> \u003cskill>/SKILL.md\n for parent in Path(__file__).resolve().parents:\n candidate = parent / \"SKILL.md\"\n if candidate.is_file():\n return candidate\n raise FileNotFoundError(\"SKILL.md not found above \" + __file__)\n\n\ndef _extract_prefix_block(skill_md: str) -> str:\n section = skill_md.split(\"## Resolving the script prefix\", 1)[1]\n match = re.search(r\"```bash\\n(.*?)\\n```\", section, re.S)\n if match is None:\n raise ValueError(\"no ```bash block under '## Resolving the script prefix'\")\n return match.group(1) + \"\\n\"\n\n\n_FETCH_STUB = \"\"\"\\\nimport json, os, sys\nwith open({record!r}, \"a\") as fh:\n fh.write(json.dumps({{\"path\": os.path.realpath(__file__),\n \"argv\": sys.argv[1:]}}) + \"\\\\n\")\n\"\"\"\n\n# Appended after the extracted block: report what it resolved, then invoke the\n# entry script exactly as the rest of SKILL.md does.\n_EPILOGUE = \"\"\"\nprintf 'SKILL_ROOT=%s\\\\nprefix=%s\\\\n' \"$SKILL_ROOT\" \"$prefix\"\npython3 \"$prefix/fetch_dc.py\" --session probe-test\n\"\"\"\n\n\nclass PrefixBlockTest(unittest.TestCase):\n SHELLS = [s for s in (\"bash\", \"zsh\") if shutil.which(s)]\n\n @classmethod\n def setUpClass(cls) -> None:\n cls.block = _extract_prefix_block(_find_skill_md().read_text())\n\n def setUp(self) -> None:\n self.tmp = Path(tempfile.mkdtemp(prefix=\"d360-prefix-test-\"))\n self.addCleanup(shutil.rmtree, self.tmp, True)\n self.record = self.tmp / \"calls.jsonl\"\n self.home = self.tmp / \"home\"\n self.home.mkdir()\n self.bin = self.tmp / \"bin\"\n self.bin.mkdir()\n (self.bin / \"python3\").symlink_to(sys.executable)\n self.script = self.tmp / \"block.sh\"\n self.script.write_text(self.block + _EPILOGUE)\n\n # -- helpers ---------------------------------------------------------\n\n def _make_scripts(self, root: Path) -> Path:\n scripts = root / \"scripts\"\n scripts.mkdir(parents=True)\n (scripts / \"fetch_dc.py\").write_text(_FETCH_STUB.format(record=str(self.record)))\n return scripts\n\n def _run(self, shell: str, skill_root: str | None):\n if self.record.exists():\n self.record.unlink()\n env = {\"PATH\": f\"{self.bin}:/usr/bin:/bin\", \"HOME\": str(self.home)}\n if skill_root is not None:\n env[\"SKILL_ROOT\"] = skill_root\n cmd = [\"env\", \"-i\", *(f\"{k}={v}\" for k, v in env.items()),\n shell, str(self.script)]\n proc = subprocess.run(\n cmd, capture_output=True, text=True, timeout=60, cwd=self.tmp,\n )\n calls = []\n if self.record.exists():\n calls = [json.loads(l) for l in self.record.read_text().splitlines()]\n return proc, calls\n\n def _assert_resolved(self, proc, calls, root: Path) -> None:\n self.assertEqual(proc.returncode, 0, proc.stderr)\n self.assertIn(f\"SKILL_ROOT={root}\\n\", proc.stdout)\n self.assertIn(f\"prefix={root}/scripts\\n\", proc.stdout)\n self.assertEqual(len(calls), 1, calls)\n self.assertEqual(\n calls[0][\"path\"], str((root / \"scripts\" / \"fetch_dc.py\").resolve())\n )\n self.assertEqual(calls[0][\"argv\"], [\"--session\", \"probe-test\"])\n\n # -- scenarios -------------------------------------------------------\n\n def test_shells_available(self) -> None:\n self.assertIn(\"bash\", self.SHELLS)\n\n def test_a_shipped_layout_exported_root(self) -> None:\n root = self.tmp / \"skill\"\n self._make_scripts(root)\n for shell in self.SHELLS:\n with self.subTest(shell=shell):\n self._assert_resolved(*self._run(shell, str(root)), root)\n\n def test_b_scripts_only_under_artifacts_of_exported_root(self) -> None:\n staged = self.tmp / \"staged\" / _SKILL_NAME\n self._make_scripts(staged / \"artifacts\")\n (staged / \"SKILL.md\").write_text(\"# staged\\n\")\n for shell in self.SHELLS:\n with self.subTest(shell=shell):\n self._assert_resolved(\n *self._run(shell, str(staged)), staged / \"artifacts\"\n )\n\n def test_c_artifacts_found_via_install_root_probe(self) -> None:\n staged = self.home / \".claude\" / \"skills\" / _SKILL_NAME\n self._make_scripts(staged / \"artifacts\")\n for shell in self.SHELLS:\n with self.subTest(shell=shell):\n self._assert_resolved(\n *self._run(shell, None), staged / \"artifacts\"\n )\n\n def test_d_candidate_root_preferred_over_its_artifacts(self) -> None:\n root = self.tmp / \"skill\"\n self._make_scripts(root)\n self._make_scripts(root / \"artifacts\")\n for shell in self.SHELLS:\n with self.subTest(shell=shell):\n self._assert_resolved(*self._run(shell, str(root)), root)\n\n def test_e_earlier_candidate_artifacts_beats_later_candidate_root(self) -> None:\n staged = self.tmp / \"staged\" / _SKILL_NAME\n self._make_scripts(staged / \"artifacts\")\n self._make_scripts(self.home / \".claude\" / \"skills\" / _SKILL_NAME)\n for shell in self.SHELLS:\n with self.subTest(shell=shell):\n self._assert_resolved(\n *self._run(shell, str(staged)), staged / \"artifacts\"\n )\n\n def test_f_no_scripts_anywhere_reports_guard_message(self) -> None:\n empty = self.tmp / \"empty\" / _SKILL_NAME\n (empty / \"artifacts\").mkdir(parents=True)\n for shell in self.SHELLS:\n with self.subTest(shell=shell):\n proc, calls = self._run(shell, str(empty))\n self.assertEqual(proc.returncode, 1)\n self.assertIn(f\"{_SKILL_NAME}: scripts not found\", proc.stderr)\n self.assertNotIn(\"prefix=\", proc.stdout)\n self.assertEqual(calls, [])\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":6962,"content_sha256":"7e45f2afee69e95d409b7aa40b5fc268d839316f5a9751a0037aa4027d8a1c42"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"agentforce-d360-analyze — Data Cloud 360° session view","type":"text"}]},{"type":"paragraph","content":[{"text":"Hierarchical session reconstruction from Data Cloud STDM + GenAI DMOs for one Agentforce session. Three stages — fetch → assemble → render. Typical wall-clock: ~10–30s for a ~15-turn session.","type":"text"}]},{"type":"paragraph","content":[{"text":"The pipeline is ","type":"text"},{"text":"DC-only","type":"text","marks":[{"type":"strong"}]},{"text":": it reads runtime audit rows that Data Cloud has materialized. It is ","type":"text"},{"text":"not","type":"text","marks":[{"type":"strong"}]},{"text":" a runtime-availability tool — see \"DC-only blind spot\" below for what this skill cannot answer.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"If the user hasn't given enough to proceed","type":"text"}]},{"type":"paragraph","content":[{"text":"When invoked with no session id AND no discovery criteria, print this block ","type":"text"},{"text":"verbatim","type":"text","marks":[{"type":"strong"}]},{"text":" — do not paraphrase, do not pre-run any script. Trigger condition: the input is empty OR contains no session-id shape (neither a UUID nor a ","type":"text"},{"text":"0Mw…","type":"text","marks":[{"type":"code_inline"}]},{"text":" messaging id) AND no discovery expression (no time phrase / ","type":"text"},{"text":"--agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"--channel","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"--outcome","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"--grep","type":"text","marks":[{"type":"code_inline"}]},{"text":" / verbs like \"find\" / \"list\").","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Which session should I pull from Data Cloud, and in which org?","type":"text"}]},{"type":"paragraph","content":[{"text":"I need:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Session id","type":"text","marks":[{"type":"strong"}]},{"text":" — either an Agent Session UUID (","type":"text"},{"text":"019db7f6-…","type":"text","marks":[{"type":"code_inline"}]},{"text":") or a MessagingSession id (","type":"text"},{"text":"0Mw…","type":"text","marks":[{"type":"code_inline"}]},{"text":", 15/18 chars).","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No session id?","type":"text","marks":[{"type":"strong"}]},{"text":" — Tell me what you remember and I'll find it: how recent (e.g. \"last 2 hours\", \"today\", a date), which agent, which channel (Messaging / Builder / Voice), how it ended (escalated, user ended, transferred, timed out), or a phrase from the conversation. I'll show matching sessions as a numbered list — you pick one, I pull it.","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Org alias","type":"text","marks":[{"type":"strong"}]},{"text":" — for ","type":"text"},{"text":"sf","type":"text","marks":[{"type":"code_inline"}]},{"text":" CLI auth (the alias you configured with ","type":"text"},{"text":"sf org login","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Artifacts land in ","type":"text"},{"text":"~/.vibe/data/agentforce-d360-analyze/\u003corg_id15>/\u003cagent>__\u003cver>/\u003csession_id>/","type":"text","marks":[{"type":"code_inline"}]},{"text":" (override per-script with ","type":"text"},{"text":"--data-dir \u003cpath>","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Session id forms — UUID or MessagingSession id","type":"text"}]},{"type":"paragraph","content":[{"text":"Both forms are accepted on ","type":"text"},{"text":"--session","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Form","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Example","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Resolution","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Agent Session UUID","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"019dface-0000-7000-8000-000000000002","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pass-through","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MessagingSession id (","type":"text"},{"text":"0Mw","type":"text","marks":[{"type":"code_inline"}]},{"text":" prefix)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0MwTESTMSG12345AAA","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Resolved via ","type":"text"},{"text":"resolve_session.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" — live DC lookup on first fetch, disk-first thereafter","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Multi-match is real.","type":"text","marks":[{"type":"strong"}]},{"text":" One MessagingSession id can map to multiple Agent Session UUIDs. On multi-match the resolver prints every candidate and exits non-zero; the user re-invokes with a specific UUID.","type":"text"}]},{"type":"paragraph","content":[{"text":"Artifacts always land under ","type":"text"},{"text":"~/.vibe/data/agentforce-d360-analyze/\u003corg_id15>/\u003cagent>__\u003cver>/\u003csession_id>/","type":"text","marks":[{"type":"code_inline"}]},{"text":" (default; overridable per-script with ","type":"text"},{"text":"--data-dir \u003cpath>","type":"text","marks":[{"type":"code_inline"}]},{"text":") — the messaging id is a lookup key only, never a directory name. The dominant agent (first in ","type":"text"},{"text":"sorted(agents_observed)","type":"text","marks":[{"type":"code_inline"}]},{"text":") names the ","type":"text"},{"text":"\u003cagent>__\u003cver>/","type":"text","marks":[{"type":"code_inline"}]},{"text":" segment.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Resolving the script prefix","type":"text"}]},{"type":"paragraph","content":[{"text":"\u003cSKILL_DIR>","type":"text","marks":[{"type":"code_inline"}]},{"text":" = the absolute path to ","type":"text"},{"text":"this skill's own directory","type":"text","marks":[{"type":"strong"}]},{"text":" (the folder holding this ","type":"text"},{"text":"SKILL.md","type":"text","marks":[{"type":"code_inline"}]},{"text":"); resolve it from the skill path in context and substitute it below. Runtimes install skills in different places (","type":"text"},{"text":"~/.claude/skills/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"~/.vibe/skills/","type":"text","marks":[{"type":"code_inline"}]},{"text":", a plugin cache, a project ","type":"text"},{"text":".claude/skills/","type":"text","marks":[{"type":"code_inline"}]},{"text":"), so the block also probes the known install roots at runtime and uses the first that actually contains this skill's scripts. An exported ","type":"text"},{"text":"SKILL_ROOT","type":"text","marks":[{"type":"code_inline"}]},{"text":" wins.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"_skill=agentforce-d360-analyze\nfor _c in \"${SKILL_ROOT:-}\" \"\u003cSKILL_DIR>\" \\\n \"${CLAUDE_PLUGIN_ROOT:+$CLAUDE_PLUGIN_ROOT/skills/$_skill}\" \\\n \"${VIBES_SKILLS_DIR:+$VIBES_SKILLS_DIR/$_skill}\" \\\n \"$HOME/.claude/skills/$_skill\" \"$PWD/.claude/skills/$_skill\" \\\n \"${PLUGIN_ROOT:-$HOME/.vibe/skills}/$_skill\"; do\n [ -n \"$_c\" ] || continue\n # Some stagers (ADK eval) nest the bundled files under \u003cskill>/artifacts/.\n for _r in \"$_c\" \"$_c/artifacts\"; do\n [ -f \"$_r/scripts/fetch_dc.py\" ] && { SKILL_ROOT=\"$_r\"; break 2; }\n done\ndone\n[ -f \"${SKILL_ROOT:-}/scripts/fetch_dc.py\" ] || { echo \"$_skill: scripts not found — set SKILL_ROOT to this skill's directory\" >&2; exit 1; }\nprefix=\"$SKILL_ROOT/scripts\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Every subsequent invocation in this doc uses ","type":"text"},{"text":"\"$prefix/...\"","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Session discovery (no id yet)","type":"text"}]},{"type":"paragraph","content":[{"text":"When the user doesn't have a session id, run ","type":"text"},{"text":"discover_sessions.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" against the STDM session DMO. Prints a numbered picker; user picks one; proceed with the chosen UUID.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 \"$prefix/discover_sessions.py\" [--org \u003calias>] [filters...]","type":"text"}]},{"type":"paragraph","content":[{"text":"--org","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" is optional.","type":"text","marks":[{"type":"strong"}]},{"text":" \"My org\", \"our org\", or no alias at all means the sf CLI default target org — omit ","type":"text"},{"text":"--org","type":"text","marks":[{"type":"code_inline"}]},{"text":" and the script uses ","type":"text"},{"text":"sf config get target-org","type":"text","marks":[{"type":"code_inline"}]},{"text":". Pass ","type":"text"},{"text":"--org \u003calias>","type":"text","marks":[{"type":"code_inline"}]},{"text":" only when the user names an alias. If no default is set the script exits with \"no --org given and no default target org set\"; relay its ","type":"text"},{"text":"sf config set target-org \u003calias>","type":"text","marks":[{"type":"code_inline"}]},{"text":" hint and ask the user for an alias.","type":"text"}]},{"type":"paragraph","content":[{"text":"Filters","type":"text","marks":[{"type":"strong"}]},{"text":" (all optional): ","type":"text"},{"text":"--since \u003cexpr>","type":"text","marks":[{"type":"code_inline"}]},{"text":" (default last 24h; accepts \"last 2 hours\", \"today\", ISO dates), ","type":"text"},{"text":"--agent \u003capi-name>","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--channel \u003cMessaging|Builder|Voice>","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--outcome \u003cUSER_ENDED|ESCALATED|TRANSFERRED|TIMEOUT|NOT_SET>","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--grep \u003csubstring>","type":"text","marks":[{"type":"code_inline"}]},{"text":" (conversation text), ","type":"text"},{"text":"--tz \u003cIANA>","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--limit \u003cN>","type":"text","marks":[{"type":"code_inline"}]},{"text":" (default 20).","type":"text"}]},{"type":"paragraph","content":[{"text":"Output","type":"text","marks":[{"type":"strong"}]},{"text":": markdown table with ","type":"text"},{"text":"#","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"UUID","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Start (UTC)","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Agent","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Channel","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Duration","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Outcome","type":"text","marks":[{"type":"code_inline"}]},{"text":". User replies with a number; proceed with that UUID.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Pipeline — three stages","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"fetch_dc.py → 24 dc.\u003cname>.json + dc._session_manifest.json (DC Query REST waterfall, 5 waves)\nassemble_dc.py → dc._session_tree.json (pure in-memory hierarchical join)\nrender_dc.py → dc._session_summary.md (human summary, multi-section)","type":"text"}]},{"type":"paragraph","content":[{"text":"Each stage is independently runnable. ","type":"text"},{"text":"fetch_dc.py --session \u003csid> [--org \u003calias>]","type":"text","marks":[{"type":"code_inline"}]},{"text":" chains all three by default.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Invocation","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 \"$prefix/fetch_dc.py\" --session \u003csession-id-or-messaging-id> [--org \u003calias>]","type":"text"}]},{"type":"paragraph","content":[{"text":"--org","type":"text","marks":[{"type":"code_inline"}]},{"text":" is optional here too: without it (the user said \"my org\" or named no alias) the CLI default target org is used. With no default set, ","type":"text"},{"text":"fetch_dc.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" reports ","type":"text"},{"text":"DC_ACCESS_DENIED","type":"text","marks":[{"type":"code_inline"}]},{"text":" with reason ","type":"text"},{"text":"no_org","type":"text","marks":[{"type":"code_inline"}]},{"text":" (exit 10 when headless) — ask the user for an alias and re-run with ","type":"text"},{"text":"--org \u003calias>","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Flags: ","type":"text"},{"text":"--verbose","type":"text","marks":[{"type":"code_inline"}]},{"text":" for per-DMO row counts; ","type":"text"},{"text":"--no-assemble","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"--no-render","type":"text","marks":[{"type":"code_inline"}]},{"text":" to stop early. All entry scripts (","type":"text"},{"text":"fetch_dc.py","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"assemble_dc.py","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"render_dc.py","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"resolve_session.py","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"discover_sessions.py","type":"text","marks":[{"type":"code_inline"}]},{"text":") accept ","type":"text"},{"text":"--data-dir \u003cpath>","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"--cache-dir \u003cpath>","type":"text","marks":[{"type":"code_inline"}]},{"text":" to override the default ","type":"text"},{"text":"~/.vibe/{data,cache}/agentforce-d360-analyze/","type":"text","marks":[{"type":"code_inline"}]},{"text":" roots — pass these when the host runtime needs artifacts under a different distribution layout.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Output artifacts","type":"text"}]},{"type":"paragraph","content":[{"text":"Everything lands under ","type":"text"},{"text":"~/.vibe/data/agentforce-d360-analyze/\u003corg_id15>/\u003cagent>__\u003cver>/\u003csession_id>/","type":"text","marks":[{"type":"code_inline"}]},{"text":" (default; override with ","type":"text"},{"text":"--data-dir \u003cpath>","type":"text","marks":[{"type":"code_inline"}]},{"text":"):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"dc.sessions.json dc.steps.json dc.gateway_requests.json\ndc.interactions.json dc.messages.json dc.gateway_responses.json\ndc.participants.json dc.generations.json dc.gateway_request_llm.json\ndc.content_quality.json dc.content_category.json dc.gateway_request_metadata.json\ndc.tags.json dc.tag_definitions.json dc.gateway_request_tags.json\ndc.tag_associations.json dc.tag_definition_associations.json\ndc.feedback.json dc.feedback_details.json dc.gateway_records.json\ndc.moments.json dc.moment_interactions.json\ndc.telemetry_spans.json dc.app_generation.json\n\ndc._session_manifest.json (per-DMO row counts + empties)\ndc._session_tree.json (hierarchical join — session → interactions → steps → messages → generations → gateway)\ndc._session_summary.md (rendered human summary)","type":"text"}]},{"type":"paragraph","content":[{"text":"Zero-row queries are recorded in the manifest with ","type":"text"},{"text":"status: empty","type":"text","marks":[{"type":"code_inline"}]},{"text":"; no file is written. ","type":"text"},{"text":"assemble_dc","type":"text","marks":[{"type":"code_inline"}]},{"text":" tolerates missing files. See ","type":"text"},{"text":"references/artifacts.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the full read order.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"The DC-only blind spot — read before committing to a root cause","type":"text"}]},{"type":"paragraph","content":[{"text":"DC alone answers ","type":"text"},{"text":"what happened","type":"text","marks":[{"type":"strong"}]},{"text":" — steps that ran, generations that fired, gateway requests that were logged. It does NOT answer ","type":"text"},{"text":"what could have happened but didn't","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Which ","type":"text"},{"text":"topics were eligible","type":"text","marks":[{"type":"strong"}]},{"text":" for the classifier on a given turn (this lives in runtime planner telemetry, not DC).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Which ","type":"text"},{"text":"actions were declared","type":"text","marks":[{"type":"strong"}]},{"text":" on a topic vs. which ","type":"text"},{"text":"survived rule expressions","type":"text","marks":[{"type":"strong"}]},{"text":" and were actually offered to the LLM.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Why the LLM picked one topic/action over another (the full prompt + response text only lives in the planner runtime telemetry).","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"If the user's question is about ","type":"text"},{"text":"why a particular topic or action was or wasn't used","type":"text","marks":[{"type":"em"}]},{"text":", DC-only is almost never sufficient. ","type":"text"},{"text":"Tell the user","type":"text","marks":[{"type":"strong"}]},{"text":": \"Availability questions need the runtime planner trace for that turn — which is outside this skill's Data Cloud surface. Check the platform telemetry that mirrors the planner's logged decisions.\" Don't fabricate a root cause from runtime-only evidence.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"What DC IS good at","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"What ran","type":"text","marks":[{"type":"strong"}]},{"text":" — every step, every LLM call, every gateway request + response, in order, with timestamps and durations. Good for \"walk me through the session\".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"What the user saw","type":"text","marks":[{"type":"strong"}]},{"text":" — full message transcript (user + agent), ordered.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"What the LLM produced","type":"text","marks":[{"type":"strong"}]},{"text":" — generations, token counts, trust scores (toxicity, instruction adherence, content-category breakdown from ","type":"text"},{"text":"content_quality","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"content_category","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tool invocations","type":"text","marks":[{"type":"strong"}]},{"text":" — action calls, inputs, outputs, errors (from ","type":"text"},{"text":"gateway_request_metadata","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"gateway_records","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Feedback + flags","type":"text","marks":[{"type":"strong"}]},{"text":" — user feedback, escalation markers, session-end type.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Audit integrity","type":"text","marks":[{"type":"strong"}]},{"text":" — the 1:1 invariant between GatewayRequest and GatewayResponse is checked; drift is flagged in ","type":"text"},{"text":"counts.audit_chain_1to1_ok","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tool","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Required","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf","type":"text","marks":[{"type":"code_inline"}]},{"text":" CLI (authenticated against the target org)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"yes — ","type":"text"},{"text":"sf org login web --alias \u003calias>","type":"text","marks":[{"type":"code_inline"}]},{"text":", and the CLI must provide ","type":"text"},{"text":"sf org auth show-access-token","type":"text","marks":[{"type":"code_inline"}]},{"text":" (startup preflight enforces this)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Data Cloud enabled on the target org","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"yes — the STDM + GenAI DMOs must have materialized for the session","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Python 3.10+","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"yes — pipeline scripts","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Typical prompts — what they map to","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"User says","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Skill does","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"Trace session ","type":"text","marks":[{"type":"em"}]},{"text":"\u003cuuid>","type":"text","marks":[{"type":"code_inline"},{"type":"em"}]},{"text":" in my-org\"","type":"text","marks":[{"type":"em"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"fetch_dc.py --session \u003cuuid> --org my-org","type":"text","marks":[{"type":"code_inline"}]},{"text":" → assemble → render","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"Summarize what happened in ","type":"text","marks":[{"type":"em"}]},{"text":"0Mw…","type":"text","marks":[{"type":"code_inline"},{"type":"em"}]},{"text":"\"","type":"text","marks":[{"type":"em"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Resolve ","type":"text"},{"text":"0Mw…","type":"text","marks":[{"type":"code_inline"}]},{"text":" → UUID, then full DC pipeline","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"Find escalated sessions today in my-org on Messaging\"","type":"text","marks":[{"type":"em"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"discover_sessions.py --since today --outcome ESCALATED --channel Messaging","type":"text","marks":[{"type":"code_inline"}]},{"text":", print picker, user picks, then DC pipeline","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"Walk me through this session\"","type":"text","marks":[{"type":"em"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Same as trace — read the rendered summary top to bottom","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"What comes back to the user","type":"text"}]},{"type":"paragraph","content":[{"text":"After the pipeline completes, the rendered ","type":"text"},{"text":"dc._session_summary.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" carries these top-level sections:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Session identity","type":"text","marks":[{"type":"strong"}]},{"text":" — UUID, start/end, duration, agent, channel, end type, participant counts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Session bootstrap","type":"text","marks":[{"type":"strong"}]},{"text":" — channel mode + bootstrap variables (","type":"text"},{"text":"identity.mode","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"identity.bootstrap_variables","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ID reference","type":"text","marks":[{"type":"strong"}]},{"text":" — full UUIDs for everything truncated in the hierarchical trace","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Transcript","type":"text","marks":[{"type":"strong"}]},{"text":" — USER ↔ AGENT narrative per TURN interaction","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Complete hierarchical trace","type":"text","marks":[{"type":"strong"}]},{"text":" — Interaction → Step → Generation → GatewayRequest, with ","type":"text"},{"text":"+start + duration = +end","type":"text","marks":[{"type":"code_inline"}]},{"text":" math","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Per-turn summary","type":"text","marks":[{"type":"strong"}]},{"text":" — one row per interaction","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Planner LLM calls (full prompts + responses)","type":"text","marks":[{"type":"strong"}]},{"text":" — opt-in via ","type":"text"},{"text":"--show-prompts","type":"text","marks":[{"type":"code_inline"}]},{"text":"; suppressed by default","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Visual analysis","type":"text","marks":[{"type":"strong"}]},{"text":" — gantt + LLM-call overlay","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Session counts","type":"text","marks":[{"type":"strong"}]},{"text":" — engineer-facing table of manifest counts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Empties diagnostics","type":"text","marks":[{"type":"strong"}]},{"text":" — one row per DMO with ","type":"text"},{"text":"rows == 0","type":"text","marks":[{"type":"code_inline"}]},{"text":" and a populated ","type":"text"},{"text":"_unavailable_reason","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Catalog (session-filtered)","type":"text","marks":[{"type":"strong"}]},{"text":" — TagDefinitions / TagDefinitionAssociations / Tags filtered to agents observed in the session","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"For deep-dive, open ","type":"text"},{"text":"dc._session_tree.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" — the single source of truth the summary was rendered from. See ","type":"text"},{"text":"references/dc_pipeline_contract.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the full pipeline contract and ","type":"text"},{"text":"references/dc_dmo_fields.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for per-DMO field reference.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Caveats","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"gateway_requests_dropped_by_stdm","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — when DC reports zero ","type":"text"},{"text":"gateway_requests","type":"text","marks":[{"type":"code_inline"}]},{"text":" rows but runtime telemetry would show LLM calls did fire, this skill cannot definitively distinguish \"STDM exporter dropped writes\" from \"logging genuinely disabled at the source\". The session is reported as ","type":"text"},{"text":"planner_ran_no_gateway_logs","type":"text","marks":[{"type":"code_inline"}]},{"text":"; the operator can check platform telemetry to disambiguate. See ","type":"text"},{"text":"references/dc_pipeline_contract.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" §2.8.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Latency","type":"text","marks":[{"type":"strong"}]},{"text":" — Generation and GatewayRequest carry single-write timestamps, not start/end pairs. The renderer does not compute \"latencies\" between them — that delta reflects DC's serialization order, not how long the LLM call took.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Data Cloud materialization lag","type":"text","marks":[{"type":"strong"}]},{"text":" — fresh sessions may show ","type":"text"},{"text":"interactions_not_materialized_yet","type":"text","marks":[{"type":"code_inline"}]},{"text":" if STDM hasn't caught up. Re-run after a minute or two.","type":"text"}]}]}]}]},"metadata":{"date":"2026-11-02","name":"agentforce-d360-analyze","author":"@skillopedia","source":{"stars":1139,"repo_name":"sf-skills","origin_url":"https://github.com/forcedotcom/sf-skills/blob/HEAD/skills/agentforce-d360-analyze/SKILL.md","repo_owner":"forcedotcom","body_sha256":"d9e5642b8743a600a5b7b33208cf6743e2e7036d1a081444de1340523b524bb8","cluster_key":"c9178b871a305911cd4c8a0b308f456b4d0ffa9ffd98a27cf219f3f3ee916ac2","clean_bundle":{"format":"clean-skill-bundle-v1","source":"forcedotcom/sf-skills/skills/agentforce-d360-analyze/SKILL.md","text_bytes":683055,"source_commit":"a8a18ce1e4ba12e69b4073eab98ce31bfbd8271e","skills_sh_rank":4155,"attachment_count":75,"text_attachments":75,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/agentforce-d360-analyze/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"integrations-apis","category_label":"Integrations"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"integrations-apis","metadata":{"domains":["Agentforce","Data 360"],"version":"1.0","cliTools":[{"tool":["python3"],"semver":">=3.10.0"},{"tool":["sf"],"semver":">=2.0.0"}],"minApiVersion":"66.0","relatedSkills":["agentforce-architecture-analyze"]},"import_tag":"skills-sh-top20k-2026-11","description":"Data Cloud 360° view of a single Agentforce session. TRIGGER when user asks to trace, inspect, summarize, or describe a specific Agentforce session by session id (Agent Session UUID `019d…` or MessagingSession id `0Mw…`). Also triggers on session discovery — find/list/search sessions by time, agent, channel, outcome, or conversation text — when the user has no session id yet. DO NOT TRIGGER for design-time architecture questions (use agentforce-architecture-analyze instead) or for runtime perf/latency/SLO questions that require platform telemetry beyond Data Cloud."}},"renderedAt":1793666125559}

agentforce-d360-analyze — Data Cloud 360° session view Hierarchical session reconstruction from Data Cloud STDM + GenAI DMOs for one Agentforce session. Three stages — fetch → assemble → render. Typical wall-clock: 10–30s for a 15-turn session. The pipeline is DC-only : it reads runtime audit rows that Data Cloud has materialized. It is not a runtime-availability tool — see "DC-only blind spot" below for what this skill cannot answer. If the user hasn't given enough to proceed When invoked with no session id AND no discovery criteria, print this block verbatim — do not paraphrase, do not pre-…