diff --git a/backend/tests/test_gemini_schema_support.py b/backend/tests/test_gemini_schema_support.py index cd66083..cce50dc 100644 --- a/backend/tests/test_gemini_schema_support.py +++ b/backend/tests/test_gemini_schema_support.py @@ -1,28 +1,8 @@ import json +import os +import pytest from typing import Optional from pydantic import BaseModel, Field -from google.genai.types import GenerateContentResponse, GenerateContentConfig - - -from utils.llm_provider import get_google_llm_client, get_large_model - - -class HeadingDescription(BaseModel): - heading: str = Field( - description="Heading of the slide", min_length=10, max_length=20 - ) - description: str = Field( - description="Description of the slide", min_length=40, max_length=200 - ) - - -class SlideContentTest(BaseModel): - title: str = Field(description="Title of the slide", min_length=10, max_length=20) - first_content: HeadingDescription = Field(description="First content of the slide") - second_content: HeadingDescription = Field( - description="Second content of the slide" - ) - third_content: HeadingDescription = Field(description="Third content of the slide") class ColumnContentModel(BaseModel): @@ -37,34 +17,55 @@ class TwoColumnSlideModel(BaseModel): description="Title of the slide", ) subtitle: Optional[str] = Field( + default=None, min_length=3, max_length=150, description="Optional subtitle or description", ) - leftColumn: ColumnContentModel = Field( - description="Left column content", - ) - rightColumn: ColumnContentModel = Field( - description="Right column content", - ) + leftColumn: ColumnContentModel = Field(description="Left column content") + rightColumn: ColumnContentModel = Field(description="Right column content") backgroundImage: Optional[str] = Field( - description="URL to background image for the slide" + default=None, + description="URL to background image for the slide", ) +@pytest.mark.skipif( + not os.getenv("GOOGLE_API_KEY"), + reason="GOOGLE_API_KEY not set — skipping Gemini API test", +) def test_gemini_schema_support(): - response: GenerateContentResponse = get_google_llm_client().models.generate_content( - model=get_large_model(), + import google.genai as genai + from google.genai.types import GenerateContentConfig + + client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"]) + model = os.getenv("GOOGLE_MODEL", "gemini-2.0-flash") + + response = client.models.generate_content( + model=model, contents=[ - "Generate a slide for a presentation", - "The slide should have a title and two contents", - "The title should be a short title for the slide", - "The contents should be a heading and a description", - "The heading should be a short heading for the slide", + "Generate a slide for a presentation about renewable energy.", + "The slide should have a title, subtitle, and two columns (left and right).", ], config=GenerateContentConfig( response_mime_type="application/json", response_schema=TwoColumnSlideModel.model_json_schema(), ), ) - print(response.text) + + assert response is not None, "Response from Gemini was None" + assert response.text, "Response text is empty" + + data = json.loads(response.text) + assert "title" in data, "Response missing 'title' field" + assert "leftColumn" in data, "Response missing 'leftColumn' field" + assert "rightColumn" in data, "Response missing 'rightColumn' field" + assert isinstance(data["leftColumn"], dict), "'leftColumn' should be an object" + assert "title" in data["leftColumn"], "'leftColumn' missing 'title'" + assert "content" in data["leftColumn"], "'leftColumn' missing 'content'" + + # Validate full model parse + slide = TwoColumnSlideModel.model_validate(data) + assert len(slide.title) >= 3, "Title too short" + assert len(slide.leftColumn.content) >= 10, "Left column content too short" + assert len(slide.rightColumn.content) >= 10, "Right column content too short" diff --git a/backend/tests/test_openai_schema_support.py b/backend/tests/test_openai_schema_support.py index f7d00b7..e146385 100644 --- a/backend/tests/test_openai_schema_support.py +++ b/backend/tests/test_openai_schema_support.py @@ -1,66 +1,10 @@ -import asyncio -import json -from typing import Optional -from pydantic import BaseModel, Field - - -from utils.llm_provider import get_llm_client, get_large_model - - -class HeadingDescription(BaseModel): - heading: str = Field( - description="Heading of the slide", min_length=10, max_length=20 - ) - description: str = Field( - description="Description of the slide", min_length=40, max_length=200 - ) - - -class SlideContentTest(BaseModel): - title: str = Field(description="Title of the slide", min_length=10, max_length=20) - first_content: HeadingDescription = Field(description="First content of the slide") - second_content: HeadingDescription = Field( - description="Second content of the slide" - ) - third_content: HeadingDescription = Field(description="Third content of the slide") - - -class ColumnContentModel(BaseModel): - title: str = Field(min_length=3, max_length=100, description="Column title") - content: str = Field(min_length=10, max_length=800, description="Column content") - - -class TwoColumnSlideModel(BaseModel): - title: str = Field( - min_length=3, - max_length=100, - description="Title of the slide", - ) - subtitle: Optional[str] = Field( - min_length=3, - max_length=150, - description="Optional subtitle or description", - ) - leftColumn: ColumnContentModel = Field( - description="Left column content", - ) - rightColumn: ColumnContentModel = Field( - description="Right column content", - ) - backgroundImage: Optional[str] = Field( - description="URL to background image for the slide" - ) +import pytest + +# This project uses a Gemini-only LLM stack (Phase 6 migration). +# OpenAI structured output is no longer part of the stack. +# See test_gemini_schema_support.py for the equivalent test. +@pytest.mark.skip(reason="OpenAI removed in Phase 6 Gemini-only migration — see test_gemini_schema_support.py") def test_openai_schema_support(): - response = asyncio.run( - get_llm_client().beta.chat.completions.parse( - model=get_large_model(), - messages=[ - {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "Generate a slide for a presentation"}, - ], - response_format=TwoColumnSlideModel, - ) - ) - print(response.choices[0].message.parsed) + pass diff --git a/backend/tests/test_pptx_creator.py b/backend/tests/test_pptx_creator.py index a10b199..9d8c621 100644 --- a/backend/tests/test_pptx_creator.py +++ b/backend/tests/test_pptx_creator.py @@ -1,4 +1,5 @@ import asyncio +import os from models.pptx_models import ( PptxAutoShapeBoxModel, PptxFillModel, @@ -8,6 +9,7 @@ from models.pptx_models import ( ) from services.pptx_presentation_creator import PptxPresentationCreator from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE +from pptx import Presentation pptx_model = PptxPresentationModel( @@ -34,7 +36,16 @@ pptx_model = PptxPresentationModel( def test_pptx_creator(): - temp_dir = "/tmp/deckforge" + temp_dir = "/tmp/deckforge_test" + os.makedirs(temp_dir, exist_ok=True) + save_path = "/tmp/deckforge_test/test_output.pptx" + pptx_creator = PptxPresentationCreator(pptx_model, temp_dir) asyncio.run(pptx_creator.create_ppt()) - pptx_creator.save("debug/test.pptx") + pptx_creator.save(save_path) + + assert os.path.exists(save_path), f"PPTX file was not created at {save_path}" + assert os.path.getsize(save_path) > 0, "PPTX file is empty" + + prs = Presentation(save_path) + assert len(prs.slides) == 1, f"Expected 1 slide, got {len(prs.slides)}" diff --git a/frontend/app/admin/audit/page.tsx b/frontend/app/admin/audit/page.tsx index 17be401..e6de59f 100644 --- a/frontend/app/admin/audit/page.tsx +++ b/frontend/app/admin/audit/page.tsx @@ -14,6 +14,7 @@ import { } from '@/components/ui/table'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; +import { Card } from '@/components/shared/Card'; import DataExportButton from '../components/DataExportButton'; import { Search } from 'lucide-react'; @@ -40,9 +41,9 @@ export default function AuditLogPage() { }; return ( -
No clients yet.
{user?.role === 'super_admin' && (Create your first client to get started.
@@ -57,21 +58,28 @@ export default function ClientsPage() {+
Policy: {client.review_policy}
diff --git a/frontend/app/admin/storage/page.tsx b/frontend/app/admin/storage/page.tsx index ffd3b7a..0ac49ae 100644 --- a/frontend/app/admin/storage/page.tsx +++ b/frontend/app/admin/storage/page.tsx @@ -16,7 +16,8 @@ import { AlertTriangle, RefreshCw, } from 'lucide-react'; -import { Card } from '@/components/ui/card'; +import { Card } from '@/components/shared/Card'; +import { StatusBadge } from '@/components/shared/StatusBadge'; import { Button } from '@/components/ui/button'; import { Dialog, @@ -68,11 +69,6 @@ function formatBytes(bytes: number): string { return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; } -const STATUS_COLORS: RecordPresentations
+Presentations
{summary.total_presentations}
Export Files
+Export Files
{summary.total_files}
Master Decks
+Master Decks
{summary.total_master_decks} - + ({summary.master_deck_files} files)
Total Size
+Total Size
{formatBytes(summary.total_size_bytes + (summary.master_deck_size_bytes || 0))}