"""End-to-end smoke test for the AI course-plan feature. Run via: odoo-bin shell -c odoo.conf -d encoach_v2 < smoke_course_plan.py Tests Phases A-E of the LangGraph-based course-plan pipeline: A. Sources / RAG (encoach.course.plan.source + SourceIndexer) B. Deliverables (services.deliverables.compute_deliverables) C. Multimedia (encoach.course.plan.media + MediaService) D. Assignments (encoach.course.plan.assignment) E. Student visibility (assignment.expand_user_ids) External-API steps (DALL-E, Polly) tolerate missing credentials by checking ``status == 'failed'`` and reporting the provider error rather than failing the whole script. """ import json import sys def hr(title): print(f"\n{'='*70}\n{title}\n{'='*70}") def fail(msg): print(f" FAIL {msg}") sys.exit(1) def ok(msg): print(f" PASS {msg}") hr("0. Locate fixtures") Plan = env['encoach.course.plan'].sudo() plan = Plan.search([], order='id desc', limit=1) if not plan: fail("No course plan in DB; seed one first") print(f" using plan #{plan.id}: {plan.name!r} (CEFR {plan.cefr_level})") Users = env['res.users'].sudo() sarah = Users.search([('login', '=', 'sarah@encoach.test')], limit=1) if not sarah: fail("sarah@encoach.test not seeded") print(f" using student user #{sarah.id}: {sarah.name}") # ---------------------------------------------------------------- Phase A hr("Phase A — Sources & RAG indexing") Source = env['encoach.course.plan.source'].sudo() src = Source.create({ 'plan_id': plan.id, 'name': 'Smoke test inline source', 'kind': 'text', 'inline_text': ( 'In Week 1 of the General English course we focus on present ' 'simple and present continuous. Reading texts target daily ' 'routines, family, and study habits at CEFR A2-B1 level. ' 'Listening scripts feature 3-minute monologues on student life ' 'with comprehension questions covering main idea and detail.' ), 'mime_type': 'text/plain', }) ok(f"created source #{src.id}") from odoo.addons.encoach_ai_course.services.source_indexer import ( SourceIndexer, ) result = SourceIndexer(env).index(src) print(f" index result: {result}") src.invalidate_recordset() if src.status == 'indexed' and src.chunks_count > 0: ok(f"indexed: {src.chunks_count} chunks, {src.extracted_chars} chars") elif src.status == 'failed': print(f" WARN indexing failed (likely no embedding API key configured): " f"{src.error}") else: print(f" WARN unexpected status: {src.status}") env.cr.commit() # ---------------------------------------------------------------- Phase B hr("Phase B — Deliverables computation") from odoo.addons.encoach_ai_course.services.deliverables import ( compute_deliverables, ) deliv = compute_deliverables(plan) summary = deliv['summary'] print(f" summary: total={summary['total']} planned={summary['planned']} " f"generated={summary['generated']} ready={summary['ready']} " f"%={summary['percent_ready']}") print(f" media (current/ready): " f"audio {summary['media']['audio']}/{summary['media']['audio_ready']} " f"image {summary['media']['image']}/{summary['media']['image_ready']} " f"video {summary['media']['video']}/{summary['media']['video_ready']}") ok(f"compute_deliverables returned {len(deliv['weeks'])} weeks") # ---------------------------------------------------------------- Phase C hr("Phase C — Multimedia generation (audio + image)") from odoo.addons.encoach_ai_course.services.media_service import MediaService mat_listening = plan.material_ids.filtered( lambda m: m.material_type == 'listening_script' )[:1] mat_reading = plan.material_ids.filtered( lambda m: m.material_type == 'reading_text' )[:1] svc = MediaService(env) if mat_listening: audio_media = svc.synthesize_audio(mat_listening, language='en-GB') print(f" audio media #{audio_media.id} status={audio_media.status} " f"err={(audio_media.error or '')[:120]!r}") if audio_media.status == 'ready': ok(f"audio bytes={audio_media.size_bytes}") else: print(f" WARN audio not ready (provider may need creds)") else: print(" SKIP no listening_script material on plan") if mat_reading: image_media = svc.generate_image(mat_reading, size='1024x1024') print(f" image media #{image_media.id} status={image_media.status} " f"err={(image_media.error or '')[:120]!r}") if image_media.status == 'ready': ok(f"image bytes={image_media.size_bytes}") else: print(f" WARN image not ready (provider may need creds)") else: print(" SKIP no reading_text material on plan") env.cr.commit() # ---------------------------------------------------------------- Phase D hr("Phase D — Assignment creation") Assignment = env['encoach.course.plan.assignment'].sudo() existing = Assignment.search([ ('plan_id', '=', plan.id), ('mode', '=', 'students'), ('state', '=', 'active'), ]) existing.unlink() asn = Assignment.create({ 'plan_id': plan.id, 'mode': 'students', 'student_user_ids': [(6, 0, [sarah.id])], 'message': 'Smoke test assignment', 'state': 'active', }) ok(f"created assignment #{asn.id} for {len(asn.student_user_ids)} student(s)") print(f" to_api_dict: {json.dumps(asn.to_api_dict(), default=str)[:200]} ...") env.cr.commit() # ---------------------------------------------------------------- Phase E hr("Phase E — Student visibility") expanded = asn.expand_user_ids() print(f" expand_user_ids -> {expanded}") if sarah.id in expanded: ok("sarah@encoach.test is included in assignment expansion") else: fail("sarah not visible in assignment.expand_user_ids()") assignments = Assignment.search([ ('state', '=', 'active'), ('student_user_ids', 'in', [sarah.id]), ]) visible_plans = {a.plan_id.id for a in assignments if sarah.id in a.expand_user_ids()} print(f" Sarah will see plans: {sorted(visible_plans)}") if plan.id in visible_plans: ok("Plan appears in student-side listing query") else: fail("Plan NOT in student-side listing") hr("DONE — Course-plan smoke test passed (provider warnings are OK)") print("Test source / assignment created. Review in admin UI on plan #%d." % plan.id)