CCAI-Demo / backend /tests /test_csv_export.py
NeonClary
Surface Credential Summary in settings + exports
64d902c
Raw
History Blame Contribute Delete
6.69 kB
from app.api.chat import (
_export_csv_table,
_export_md,
_export_txt,
)
from app.services.models import Phase, Session
from app.services.models import Participant
# Number of columns in the CSV table (kept here for tests to assert
# field-count stability if the schema ever shifts).
EXPECTED_CSV_COLUMNS = 9
def _mk_session(*, with_credentials: bool = False) -> Session:
s = Session()
s.question = "Will \"AI\" change, education? Yes, no, maybe.\nNew lines too."
p1 = Participant(
participant_id="extra_a",
name="Alice",
role_prompt="rp",
model_id="model-a",
kind="extra",
display_name="Provider/Model A",
)
p2 = Participant(
participant_id="expert_b",
name="Bob, Ph.D.",
role_prompt="rp",
model_id="model-b",
kind="expert",
display_name="Provider/Model B",
)
s.participants = [p1, p2]
s.initial_opinions = {
"extra_a": "Alice's, opinion has commas, and \"quotes\".",
"expert_b": "Bob's\nmulti-line\nopinion.",
}
s.contribution_summaries = {
"extra_a": "Stayed firm.",
"expert_b": "Pushed hard.",
}
s.final_opinions = {
"extra_a": "Final A",
"expert_b": "Final B",
}
s.messages = [
{
"speaker_id": "extra_a", "speaker_name": "Alice",
"role": "participant", "phase": Phase.CONSENSUS.value,
"text": "Final consensus statement A",
},
{
"speaker_id": "expert_b", "speaker_name": "Bob, Ph.D.",
"role": "participant", "phase": Phase.CONSENSUS.value,
"text": "Final consensus statement B",
},
]
s.final_report = {"kind": "majority", "text": "Group decided X."}
if with_credentials:
s.credential_summary = [
{
"participant_id": "extra_a",
"name": "Alice",
"expertise": "Comparative education researcher.",
"personality": "Calm, evidence-driven.",
"credibility_for_question": 0.78,
"bias_to_watch": "Tends to over-trust meta-analyses.",
},
{
"participant_id": "expert_b",
"name": "Bob, Ph.D.",
"expertise": "K-12 classroom teacher, 20 years.",
"personality": "Combative; debates loudly.",
"credibility_for_question": 0.62,
"bias_to_watch": "Anchors on personal anecdotes.",
},
]
return s
def test_csv_export_roundtrips_through_csv_module():
"""Ensure values containing commas, quotes, and newlines get quoted
correctly per RFC 4180, and that credential columns are populated."""
import csv
import io
s = _mk_session(with_credentials=True)
out = _export_csv_table(s)
assert out["filename"] == "ccai_chat_table.csv"
parsed = list(csv.reader(io.StringIO(out["content"])))
# Header is question, then final, then blank, then column row.
assert parsed[0][0] == "Question"
assert "AI" in parsed[0][1] and "education" in parsed[0][1]
assert parsed[1][0] == "Final Group Opinion"
assert "Group decided X." in parsed[1][1]
# blank row
assert parsed[2] == []
# column header row
assert parsed[3] == [
"Participant",
"Expertise (orchestrator's read)",
"Style",
"Credibility on this question (0-1)",
"Bias to watch",
"First opinion",
"Conversation contribution",
"Revised opinion",
"Final opinion",
]
alice_row = parsed[4]
assert alice_row[0] == "Alice"
# credential columns
assert alice_row[1] == "Comparative education researcher."
assert alice_row[2] == "Calm, evidence-driven."
assert alice_row[3] == "0.78"
assert alice_row[4] == "Tends to over-trust meta-analyses."
# opinion columns - "First opinion" still preserves quotes/commas/newlines
assert "\"quotes\"" in alice_row[5]
bob_row = parsed[5]
assert bob_row[0] == "Bob, Ph.D."
assert bob_row[3] == "0.62"
assert "multi-line" in bob_row[5]
def test_csv_export_no_field_count_drift():
"""Every data row should have exactly EXPECTED_CSV_COLUMNS columns,
matching the header row, even with pathological characters and even
when credentials are missing."""
import csv
import io
s = _mk_session(with_credentials=False)
out = _export_csv_table(s)
rows = list(csv.reader(io.StringIO(out["content"])))
header = rows[3]
assert len(header) == EXPECTED_CSV_COLUMNS
for row in rows[4:]:
assert len(row) == EXPECTED_CSV_COLUMNS
def test_csv_export_blank_credentials_when_summary_missing():
"""When the orchestrator hasn't built a Credential Summary yet, the
credential columns should be present but empty - never crash."""
import csv
import io
s = _mk_session(with_credentials=False)
out = _export_csv_table(s)
rows = list(csv.reader(io.StringIO(out["content"])))
alice_row = rows[4]
# cols 1..4 are credential columns (Expertise, Style, Credibility, Bias)
assert alice_row[1] == ""
assert alice_row[2] == ""
assert alice_row[3] == ""
assert alice_row[4] == ""
# but the opinion columns should still be populated
assert "Alice" in alice_row[0]
assert alice_row[6] == "Stayed firm." # contribution summary
def test_txt_export_includes_credential_block_when_present():
s = _mk_session(with_credentials=True)
out = _export_txt(s)
body = out["content"]
assert "Credential Summary" in body
assert "Comparative education researcher." in body
assert "0.78" in body
assert "Tends to over-trust meta-analyses." in body
# Block precedes the conversation transcript
cred_idx = body.index("Credential Summary")
msg_idx = body.index("Final consensus statement A")
assert cred_idx < msg_idx
def test_txt_export_omits_credential_block_when_empty():
s = _mk_session(with_credentials=False)
out = _export_txt(s)
assert "Credential Summary" not in out["content"]
def test_md_export_includes_credential_block_when_present():
s = _mk_session(with_credentials=True)
out = _export_md(s)
body = out["content"]
assert "## Credential Summary" in body
assert "### Alice" in body
assert "### Bob, Ph.D." in body
assert "**Credibility on this question:** 0.78" in body
assert "**Bias to watch:**" in body
def test_md_export_omits_credential_block_when_empty():
s = _mk_session(with_credentials=False)
out = _export_md(s)
assert "## Credential Summary" not in out["content"]