Files
magnus919_agent-skills/fireflies/tests/test_fireflies.py
T
Magnus HedemarkandGitHub eadb82e069 fix(skills): reconcile fireflies CLI with live GraphQL schema (#290)
Fixes #289

- transcripts list: drop removed TranscriptsQueryScope type (scope is a
  String in the live schema), require [String!] for organizers and
  participants, add title/organizer-email/participant-email filters
- bites create: use the live transcript_Id argument name and the
  BitePrivacy enum (public, team, participants)
- add ergonomic commands for documented gaps found in the audit:
  askfred get, meetings update-channel, meetings share --expiry-days,
  live add-to (addToLiveMeeting), live soundbite (createLiveSoundbite),
  audio create-upload/confirm-upload (two-phase upload), users set-role
- add eval manifest (5 cases) to satisfy the modified-skill eval ratchet
- update SKILL.md, cli-reference, api-reference, source-index, workflows
  to match the audited surface and record the 2026-08-05 schema audit
2026-08-05 22:29:36 -04:00

124 lines
9.5 KiB
Python

import hashlib
import hmac
import json
import os
import subprocess
import tempfile
import threading
import unittest
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
CLI = Path(__file__).parents[1] / "scripts" / "fireflies"
class Handler(BaseHTTPRequestHandler):
calls = []
response = {"data": {"ok": True}}
status = 200
def do_POST(self):
body = self.rfile.read(int(self.headers["Content-Length"]))
self.__class__.calls.append((dict(self.headers), json.loads(body)))
self.send_response(self.__class__.status); self.send_header("Content-Type", "application/json"); self.end_headers()
self.wfile.write(json.dumps(self.__class__.response).encode())
def log_message(self, *_): pass
class FirefliesTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.server = HTTPServer(("127.0.0.1", 0), Handler)
cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True); cls.thread.start()
cls.endpoint = "http://127.0.0.1:%s/graphql" % cls.server.server_port
@classmethod
def tearDownClass(cls): cls.server.shutdown()
def run_cli(self, *args, key=False):
env = os.environ.copy(); env.pop("FIREFLIES_API_KEY", None)
if key: env["FIREFLIES_API_KEY"] = "test-key"
return subprocess.run(["python3", str(CLI), *args], text=True, capture_output=True, env=env)
def test_dry_run_needs_no_key_and_no_http(self):
Handler.calls.clear(); result=self.run_cli("transcripts","list","--dry-run","--json")
self.assertEqual(result.returncode,0); self.assertEqual(Handler.calls,[]); self.assertTrue(json.loads(result.stdout)["dry_run"])
def test_read_uses_bearer_and_payload(self):
Handler.calls.clear(); Handler.response={"data":{"users":[]}}
result=self.run_cli("--endpoint",self.endpoint,"users","list","--json",key=True)
self.assertEqual(result.returncode,0); self.assertEqual(Handler.calls[-1][0]["Authorization"],"Bearer test-key"); self.assertIn("query Users",Handler.calls[-1][1]["query"])
def test_graphql_error_json_and_exit_five(self):
Handler.response={"errors":[{"message":"denied","code":"auth_failed"}]}
result=self.run_cli("--endpoint",self.endpoint,"users","list","--json",key=True)
self.assertEqual(result.returncode,5); self.assertEqual(json.loads(result.stdout)["errors"][0]["message"],"denied"); self.assertIn("GraphQL error",result.stderr)
Handler.response={"data":{"ok":True}}
def test_http_error_with_graphql_errors_exits_five(self):
Handler.status=400; Handler.response={"errors":[{"message":"denied","code":"auth_failed"}]}
result=self.run_cli("--endpoint",self.endpoint,"users","list","--json",key=True)
self.assertEqual(result.returncode,5); self.assertEqual(json.loads(result.stdout)["errors"][0]["message"],"denied"); self.assertIn("GraphQL error",result.stderr)
Handler.status=200; Handler.response={"data":{"ok":True}}
def test_generic_guards(self):
self.assertEqual(self.run_cli("query","--document","mutation { x }","--json").returncode,2)
self.assertEqual(self.run_cli("mutation","--document","mutation { x }","--json").returncode,6)
def test_limit_json_and_help_examples(self):
self.assertEqual(self.run_cli("transcripts","list","--limit","51","--json").returncode,2)
result=self.run_cli("transcripts","list","--dry-run","--json"); json.loads(result.stdout); self.assertEqual(result.stderr,"")
self.assertIn("Examples:",self.run_cli("transcripts","list","--help").stdout)
def test_webhook_signatures(self):
with tempfile.NamedTemporaryFile("wb",delete=False) as f: f.write(b'{"event":"meeting.transcribed"}'); path=f.name
digest=hmac.new(b"test",Path(path).read_bytes(),hashlib.sha256).hexdigest()
self.assertTrue(json.loads(self.run_cli("webhook","verify","--secret","test","--signature","sha256="+digest,"--body",path,"--json").stdout)["valid"])
self.assertFalse(json.loads(self.run_cli("webhook","verify","--secret","test","--signature","sha256=bad","--body",path,"--json").stdout)["valid"])
os.unlink(path)
def test_mutation_dry_run_payload(self):
result=self.run_cli("meetings","rename","abc","--title","New","--dry-run","--json")
payload=json.loads(result.stdout)["payload"]; self.assertIn("updateMeetingTitle",payload["query"]); self.assertEqual(payload["variables"]["input"]["id"],"abc")
def test_source_faithful_dry_run_payloads(self):
live=json.loads(self.run_cli("live","add","--meeting-id","meeting-id","--action-item","Follow up","--dry-run","--json").stdout)["payload"]
self.assertIn("createLiveActionItem(input: $input) { success }",live["query"])
self.assertEqual(live["variables"],{"input":{"meeting_id":"meeting-id","prompt":"Follow up"}})
deleted=json.loads(self.run_cli("askfred","delete","thread-id","--dry-run","--json").stdout)["payload"]
self.assertIn("id title transcript_id user_id created_at",deleted["query"])
self.assertEqual(deleted["variables"],{"id":"thread-id"})
analytics=json.loads(self.run_cli("analytics","--start","2026-01-01","--end","2026-01-31","--dry-run","--json").stdout)["payload"]
self.assertIn("$startTime",analytics["query"]); self.assertIn("$endTime",analytics["query"])
self.assertEqual(analytics["variables"],{"startTime":"2026-01-01","endTime":"2026-01-31"})
transcript=json.loads(self.run_cli("transcripts","get","transcript-id","--dry-run","--json").stdout)["payload"]
self.assertIn("negative_pct neutral_pct positive_pct",transcript["query"])
audit=json.loads(self.run_cli("audit-events","--filter",'{"category":"MEETING_OPERATIONS"}',"--dry-run","--json").stdout)["payload"]
self.assertIn("events { id time action actor { user_id } resource { type id } }",audit["query"])
def test_transcripts_list_document_is_current(self):
payload=json.loads(self.run_cli("transcripts","list","--title","X","--organizers","a@b.c","--participants","d@e.f","--dry-run","--json").stdout)["payload"]
self.assertNotIn("TranscriptsQueryScope",payload["query"])
self.assertIn("$organizers: [String!]",payload["query"]); self.assertIn("$participants: [String!]",payload["query"])
self.assertIn("title: $title",payload["query"])
self.assertEqual(payload["variables"]["organizers"],["a@b.c"]); self.assertEqual(payload["variables"]["participants"],["d@e.f"])
self.assertEqual(payload["variables"]["title"],"X")
def test_bite_create_uses_live_arg_names_and_enum(self):
payload=json.loads(self.run_cli("bites","create","--transcript-id","tid","--start","0","--end","30","--privacy","participants","--dry-run","--json").stdout)["payload"]
self.assertIn("transcript_Id",payload["query"]); self.assertIn("[BitePrivacy!]",payload["query"])
self.assertEqual(payload["variables"]["privacy"],["participants"])
def test_meetings_update_channel_payload(self):
payload=json.loads(self.run_cli("meetings","update-channel","--channel-id","CH","--transcript-ids","A,B,C","--dry-run","--json").stdout)["payload"]
self.assertIn("updateMeetingChannel",payload["query"])
self.assertEqual(payload["variables"]["input"],{"transcript_ids":["A","B","C"],"channel_id":"CH"})
def test_meetings_share_expiry_payload(self):
payload=json.loads(self.run_cli("meetings","share","mid","--emails","a@b.c,d@e.f","--expiry-days","14","--dry-run","--json").stdout)["payload"]
self.assertEqual(payload["variables"]["input"]["expiry_days"],14)
self.assertEqual(payload["variables"]["input"]["emails"],["a@b.c","d@e.f"])
def test_live_add_to_payload(self):
payload=json.loads(self.run_cli("live","add-to","--meeting-link","https://x","--title","T","--duration","30","--dry-run","--json").stdout)["payload"]
self.assertIn("addToLiveMeeting",payload["query"])
self.assertEqual(payload["variables"]["meeting_link"],"https://x"); self.assertEqual(payload["variables"]["duration"],30)
def test_live_soundbite_payload(self):
payload=json.loads(self.run_cli("live","soundbite","--meeting-id","M","--prompt","P","--dry-run","--json").stdout)["payload"]
self.assertIn("createLiveSoundbite",payload["query"])
self.assertEqual(payload["variables"]["input"],{"meeting_id":"M","prompt":"P"})
def test_audio_two_phase_upload_payloads(self):
create=json.loads(self.run_cli("audio","create-upload","--content-type","audio/wav","--file-size","1000","--dry-run","--json").stdout)["payload"]
self.assertIn("createUploadUrl",create["query"]); self.assertEqual(create["variables"]["input"],{"content_type":"audio/wav","file_size":1000})
confirm=json.loads(self.run_cli("audio","confirm-upload","--meeting-id","M","--dry-run","--json").stdout)["payload"]
self.assertIn("confirmUpload",confirm["query"]); self.assertEqual(confirm["variables"]["input"],{"meeting_id":"M"})
def test_users_set_role_payload(self):
payload=json.loads(self.run_cli("users","set-role","--user-id","U","--role","admin","--dry-run","--json").stdout)["payload"]
self.assertIn("setUserRole",payload["query"]); self.assertEqual(payload["variables"],{"userId":"U","role":"admin"})
def test_askfred_get_payload(self):
payload=json.loads(self.run_cli("askfred","get","thread-id","--dry-run","--json").stdout)["payload"]
self.assertIn("askfred_thread(id: $id)",payload["query"]); self.assertEqual(payload["variables"],{"id":"thread-id"})
if __name__ == "__main__": unittest.main()