Track A: Token/cost display - Read agent usage attrs (session_prompt_tokens, session_completion_tokens, session_estimated_cost_usd) after run_conversation in streaming.py - Add input_tokens, output_tokens, estimated_cost fields to Session model - Include usage in done SSE event payload - Store usage on S.lastUsage in messages.js done handler - Render usage badge below last assistant message (input/output/cost) Track B: Subagent delegation cards - Add subagent_progress to toolIcon map with shuffle emoji - Special-case subagent_progress in buildToolCard: "Subagent" label, strip double emoji from preview, add tool-card-subagent CSS class - Indented border-left styling for subagent cards - Clean delegate_task display name Track C: Skill picker in cron create form - Add skill search input + tag chips to cron create form HTML - Skill picker JS in panels.js: search/filter, click-to-add tags, remove tag chips, pre-fetch skill list on form open - submitCronCreate sends skills array in POST body - Skill picker dropdown + tag CSS Track D: Skill linked files viewer - Add file query param to /api/skills/content endpoint - Serve linked files from skill directory with path traversal protection - Ensure linked_files key always present in skill content response - Render linked files section below SKILL.md content in preview panel - openSkillFile function for viewing individual linked files Track E: Bug fixes and code quality - Expand Session.__init__ and compact() to readable multi-line format - Remove inline import json as _j2 inside loop in streaming.py - Fix tool_calls: capture args from assistant messages, skip unresolved names - Store args snapshot in persisted tool_calls for reload display 6 new tests. Total: 421 (409 passing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
122 lines
4.4 KiB
Python
122 lines
4.4 KiB
Python
"""
|
|
Sprint 24 Tests: agentic transparency — token/cost display, session usage fields,
|
|
subagent card names, skill picker in cron, skill linked files.
|
|
"""
|
|
import json, urllib.error, urllib.request
|
|
|
|
BASE = "http://127.0.0.1:8788"
|
|
|
|
|
|
def get(path):
|
|
with urllib.request.urlopen(BASE + path, timeout=10) as r:
|
|
return json.loads(r.read()), r.status
|
|
|
|
|
|
def post(path, body=None):
|
|
data = json.dumps(body or {}).encode()
|
|
req = urllib.request.Request(BASE + path, data=data,
|
|
headers={"Content-Type": "application/json"})
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=10) as r:
|
|
return json.loads(r.read()), r.status
|
|
except urllib.error.HTTPError as e:
|
|
return json.loads(e.read()), e.code
|
|
|
|
|
|
def make_session(created_list):
|
|
d, _ = post("/api/session/new", {})
|
|
sid = d["session"]["session_id"]
|
|
created_list.append(sid)
|
|
return sid, d["session"]
|
|
|
|
|
|
# ── Session usage fields ─────────────────────────────────────────────────
|
|
|
|
def test_new_session_has_usage_fields():
|
|
"""New session should include input_tokens, output_tokens, estimated_cost."""
|
|
created = []
|
|
try:
|
|
sid, sess = make_session(created)
|
|
post("/api/session/rename", {"session_id": sid, "title": "Usage Test"})
|
|
d, status = get(f"/api/session?session_id={sid}")
|
|
assert status == 200
|
|
assert "input_tokens" in d["session"]
|
|
assert "output_tokens" in d["session"]
|
|
assert "estimated_cost" in d["session"]
|
|
assert d["session"]["input_tokens"] == 0
|
|
assert d["session"]["output_tokens"] == 0
|
|
finally:
|
|
for s in created:
|
|
post("/api/session/delete", {"session_id": s})
|
|
|
|
|
|
def test_session_compact_has_usage_fields():
|
|
"""Session list should include usage fields in compact form."""
|
|
created = []
|
|
try:
|
|
sid, _ = make_session(created)
|
|
post("/api/session/rename", {"session_id": sid, "title": "Compact Usage"})
|
|
d, status = get("/api/sessions")
|
|
assert status == 200
|
|
match = [s for s in d["sessions"] if s["session_id"] == sid]
|
|
assert len(match) == 1
|
|
assert "input_tokens" in match[0]
|
|
assert "output_tokens" in match[0]
|
|
finally:
|
|
for s in created:
|
|
post("/api/session/delete", {"session_id": s})
|
|
|
|
|
|
def test_session_usage_defaults_zero():
|
|
"""New session usage fields should default to 0/None."""
|
|
created = []
|
|
try:
|
|
sid, sess = make_session(created)
|
|
assert sess.get("input_tokens", 0) == 0
|
|
assert sess.get("output_tokens", 0) == 0
|
|
finally:
|
|
for s in created:
|
|
post("/api/session/delete", {"session_id": s})
|
|
|
|
|
|
# ── Skills content linked_files ──────────────────────────────────────────
|
|
|
|
def test_skills_content_requires_name():
|
|
"""GET /api/skills/content without name should return 400 or 500 (if skills module unavailable)."""
|
|
try:
|
|
d, status = get("/api/skills/content?file=test.md")
|
|
assert status == 400
|
|
except urllib.error.HTTPError as e:
|
|
# 500 is acceptable if the skills_tool import fails in test env
|
|
assert e.code in (400, 500)
|
|
|
|
|
|
def test_skills_content_has_linked_files_key():
|
|
"""GET /api/skills/content should return a linked_files key."""
|
|
try:
|
|
d, status = get("/api/skills")
|
|
if not d.get("skills"):
|
|
return # no skills in test env
|
|
name = d["skills"][0]["name"]
|
|
d2, status2 = get(f"/api/skills/content?name={name}")
|
|
assert status2 == 200
|
|
assert "linked_files" in d2
|
|
except urllib.error.HTTPError:
|
|
pass # skills may not work in test env
|
|
|
|
|
|
# ── Tool call integrity ──────────────────────────────────────────────────
|
|
|
|
def test_tool_calls_have_real_names():
|
|
"""Tool calls in session JSON should not have unresolved 'tool' name."""
|
|
created = []
|
|
try:
|
|
sid, _ = make_session(created)
|
|
d, status = get(f"/api/session?session_id={sid}")
|
|
assert status == 200
|
|
for tc in d["session"].get("tool_calls", []):
|
|
assert tc.get("name") != "tool", f"Unresolved name: {tc}"
|
|
finally:
|
|
for s in created:
|
|
post("/api/session/delete", {"session_id": s})
|