CX Discovery Notebook
This commit is contained in:
7
studies/202607_CX_Discovery_Workshop/tests/conftest.py
Normal file
7
studies/202607_CX_Discovery_Workshop/tests/conftest.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Make discoverylib importable even without the study venv active (the
|
||||
normal setup is ``pip install -e ".[dev]"`` into the study-local ``.venv/``)."""
|
||||
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
117
studies/202607_CX_Discovery_Workshop/tests/test_session.py
Normal file
117
studies/202607_CX_Discovery_Workshop/tests/test_session.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""Session engine pins — status, progress, checklist, and export payload.
|
||||
|
||||
Progress and status are this study's "numbers"; these are the hand-checked
|
||||
acceptance values the in-notebook gate re-pins (Pattern §4).
|
||||
"""
|
||||
|
||||
from discoverylib import (
|
||||
COMPLETE,
|
||||
IN_PROGRESS,
|
||||
NOT_STARTED,
|
||||
SKIPPED,
|
||||
TOPIC_KEYS,
|
||||
active_topic_key,
|
||||
build_session,
|
||||
normalize_status,
|
||||
progress,
|
||||
session_json,
|
||||
subtopic_checklist,
|
||||
subtopic_id,
|
||||
topic,
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_status_defaults_unknown():
|
||||
assert normalize_status("complete") == COMPLETE
|
||||
assert normalize_status(None) == NOT_STARTED
|
||||
assert normalize_status("garbage") == NOT_STARTED
|
||||
|
||||
|
||||
def test_progress_all_not_started():
|
||||
p = progress({})
|
||||
assert p.total == 8
|
||||
assert (p.completed, p.skipped, p.in_progress, p.not_started) == (0, 0, 0, 8)
|
||||
assert p.fraction == 0.0
|
||||
assert p.resolved == 0
|
||||
assert p.label == "0/8 topics complete"
|
||||
|
||||
|
||||
def test_progress_mixed_counts_and_fraction():
|
||||
status = {
|
||||
"background": COMPLETE,
|
||||
"cx_strategy": COMPLETE,
|
||||
"channels": IN_PROGRESS,
|
||||
"agent_environment": SKIPPED,
|
||||
# remaining four default to not_started
|
||||
}
|
||||
p = progress(status)
|
||||
assert (p.completed, p.skipped, p.in_progress, p.not_started) == (2, 1, 1, 4)
|
||||
assert p.resolved == 3 # complete + skipped
|
||||
assert p.fraction == 2 / 8
|
||||
assert p.label == "2/8 topics complete"
|
||||
|
||||
|
||||
def test_active_topic_prefers_in_progress_then_first_open():
|
||||
# in-progress wins even if a later topic is also in progress
|
||||
assert active_topic_key({"channels": IN_PROGRESS}) == "channels"
|
||||
# no in-progress → first not-started in canonical order
|
||||
assert active_topic_key({"background": COMPLETE}) == "cx_strategy"
|
||||
# everything resolved → None
|
||||
all_done = {k: COMPLETE for k in TOPIC_KEYS}
|
||||
assert active_topic_key(all_done) is None
|
||||
|
||||
|
||||
def test_subtopic_checklist_marks_done():
|
||||
t = topic("channels")
|
||||
done = {subtopic_id("channels", "voice_metrics")}
|
||||
items = subtopic_checklist(t, done)
|
||||
assert len(items) == len(t.subtopics)
|
||||
by_key = {i.key: i.done for i in items}
|
||||
assert by_key["voice_metrics"] is True
|
||||
assert by_key["inbound_context"] is False
|
||||
|
||||
|
||||
def test_build_session_shape_and_subtopic_done_count():
|
||||
status = {"channels": IN_PROGRESS, "background": COMPLETE}
|
||||
notes = {"channels": " 6 channels; voice ~70% "}
|
||||
done = {subtopic_id("channels", "voice_metrics"),
|
||||
subtopic_id("channels", "outbound")}
|
||||
s = build_session(status, notes, done)
|
||||
|
||||
assert len(s["topics"]) == 8
|
||||
assert s["active_topic_key"] == "channels"
|
||||
assert s["active_topic"].title == "Channels"
|
||||
assert s["agenda_minutes"] == 110
|
||||
|
||||
ch = next(ts for ts in s["topics"] if ts.key == "channels")
|
||||
assert ch.status == IN_PROGRESS
|
||||
assert ch.subtopics_done == 2
|
||||
assert ch.subtopics_total == len(topic("channels").subtopics)
|
||||
assert ch.notes == "6 channels; voice ~70%" # trimmed
|
||||
|
||||
bg = next(ts for ts in s["topics"] if ts.key == "background")
|
||||
assert bg.status == COMPLETE and bg.subtopics_done == 0
|
||||
|
||||
|
||||
def test_session_json_is_plain_and_complete():
|
||||
status = {"background": COMPLETE, "channels": IN_PROGRESS}
|
||||
notes = {"background": "3 LOBs; PCI in scope"}
|
||||
done = {subtopic_id("channels", "voice_metrics")}
|
||||
payload = session_json(build_session(status, notes, done),
|
||||
meta={"client": "Acme", "date": "2026-07-19"})
|
||||
|
||||
assert payload["study"] == "202607_CX_Discovery_Workshop"
|
||||
assert payload["meta"]["client"] == "Acme"
|
||||
assert payload["progress"]["completed"] == 1
|
||||
assert payload["progress"]["fraction_complete"] == round(1 / 8, 4)
|
||||
assert payload["active_topic"] == "channels"
|
||||
assert len(payload["topics"]) == 8
|
||||
bg = next(t for t in payload["topics"] if t["key"] == "background")
|
||||
assert bg["status"] == COMPLETE
|
||||
assert bg["notes"] == "3 LOBs; PCI in scope"
|
||||
ch = next(t for t in payload["topics"] if t["key"] == "channels")
|
||||
assert ch["subtopics_done"] == 1
|
||||
|
||||
# JSON-serializable (no dataclasses / sets leaked through)
|
||||
import json
|
||||
json.dumps(payload)
|
||||
15
studies/202607_CX_Discovery_Workshop/tests/test_staging.py
Normal file
15
studies/202607_CX_Discovery_Workshop/tests/test_staging.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""Stage/backstage detection — Mercury kernels carry MERCURY_CONFIG_DIR."""
|
||||
|
||||
from discoverylib import staging
|
||||
|
||||
|
||||
def test_backstage_prints_only_off_stage(monkeypatch, capsys):
|
||||
monkeypatch.delenv("MERCURY_CONFIG_DIR", raising=False)
|
||||
assert not staging.on_stage()
|
||||
staging.backstage("visible")
|
||||
assert capsys.readouterr().out == "visible\n"
|
||||
|
||||
monkeypatch.setenv("MERCURY_CONFIG_DIR", "/tmp/app")
|
||||
assert staging.on_stage()
|
||||
staging.backstage("hidden")
|
||||
assert capsys.readouterr().out == ""
|
||||
62
studies/202607_CX_Discovery_Workshop/tests/test_topics.py
Normal file
62
studies/202607_CX_Discovery_Workshop/tests/test_topics.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Topic bank integrity — the verbatim facilitation record.
|
||||
|
||||
These pins are the anchor guard (Pattern §4): keys are stable identities
|
||||
that captured notes and the JSON export key off, so renaming or reordering a
|
||||
topic must be a deliberate, test-breaking act — never a silent drift.
|
||||
"""
|
||||
|
||||
from discoverylib import TOPICS, TOPIC_KEYS, topic
|
||||
from discoverylib.topics import TOPIC_BY_KEY
|
||||
|
||||
|
||||
# ── Hand-checked shape (recount if you add/remove content) ───────────
|
||||
def test_topic_count_and_order():
|
||||
assert len(TOPICS) == 8
|
||||
assert TOPIC_KEYS == (
|
||||
"background",
|
||||
"cx_strategy",
|
||||
"channels",
|
||||
"agent_environment",
|
||||
"routing_automation",
|
||||
"workforce_engagement",
|
||||
"training",
|
||||
"reporting_insights",
|
||||
)
|
||||
|
||||
|
||||
def test_subtopic_and_prompt_totals():
|
||||
assert sum(len(t.subtopics) for t in TOPICS) == 26
|
||||
assert sum(t.prompt_count for t in TOPICS) == 95
|
||||
|
||||
|
||||
def test_agenda_minutes_sum():
|
||||
from discoverylib import agenda_minutes
|
||||
assert agenda_minutes() == 110
|
||||
assert agenda_minutes() == sum(t.minutes for t in TOPICS)
|
||||
|
||||
|
||||
# ── Structural invariants — hold for every topic ─────────────────────
|
||||
def test_keys_unique_and_wellformed():
|
||||
keys = [t.key for t in TOPICS]
|
||||
assert len(keys) == len(set(keys)) # unique
|
||||
for t in TOPICS:
|
||||
assert t.key.replace("_", "").isalnum() # snake_case slug
|
||||
assert t.title and t.scope # client-facing text present
|
||||
assert t.minutes > 0
|
||||
assert t.subtopics # no empty topic
|
||||
sub_keys = [st.key for st in t.subtopics]
|
||||
assert len(sub_keys) == len(set(sub_keys)) # unique within topic
|
||||
for st in t.subtopics:
|
||||
assert st.prompts # no empty sub-topic
|
||||
|
||||
|
||||
def test_lookup_helpers():
|
||||
assert topic("channels").title == "Channels"
|
||||
assert TOPIC_BY_KEY["training"].minutes == 5
|
||||
assert TOPICS[0].key == "background"
|
||||
|
||||
|
||||
def test_no_double_dollar_or_raw_markup_in_scope():
|
||||
# scope lines render on stage HTML — keep them plain text.
|
||||
for t in TOPICS:
|
||||
assert "<" not in t.scope and ">" not in t.scope
|
||||
Reference in New Issue
Block a user