""" Integration tests for the Workspace API router. Tests verify: - Endpoints are mounted and accept validated requests - Document sections include user stories, functional/non-functional requirements, and use cases - API rejects invalid input with proper validation errors """ from fastapi.testclient import TestClient class TestWorkspaceEndpointsMounted: """Verify all workspace endpoints are mounted and responding.""" def test_evaluate_endpoint_exists(self): from app.main import app client = TestClient(app) response = client.post("/api/workspace/evaluate", json={"idea": ""}) assert response.status_code == 200 def test_evaluate_rejects_missing_body(self): from app.main import app client = TestClient(app) response = client.post("/api/workspace/evaluate") assert response.status_code == 422 def test_directions_endpoint_exists(self): from app.main import app client = TestClient(app) response = client.post("/api/workspace/directions", json={"answers": {}}) assert response.status_code == 200 def test_directions_rejects_missing_body(self): from app.main import app client = TestClient(app) response = client.post("/api/workspace/directions") assert response.status_code == 422 def test_generate_endpoint_exists(self): from app.main import app client = TestClient(app) response = client.post("/api/workspace/generate", json={ "direction_id": "dir-a", "brief": "Build a task manager" }) assert response.status_code == 200 def test_generate_rejects_missing_body(self): from app.main import app client = TestClient(app) response = client.post("/api/workspace/generate") assert response.status_code == 422 def test_refine_endpoint_exists(self): from app.main import app client = TestClient(app) response = client.post("/api/workspace/refine", json={ "section_id": "sec-test", "content": "Some content", "prompt": "Make it better" }) assert response.status_code == 200 def test_refine_rejects_missing_body(self): from app.main import app client = TestClient(app) response = client.post("/api/workspace/refine") assert response.status_code == 422 class TestDocumentSectionsIncludeRequirements: """Verify document generation sections include user stories, requirements, and use cases.""" def test_sections_include_user_stories(self): """SECTIONS must include a User Stories section.""" from app.services.workspace_service import SECTIONS section_titles = [s["title"].lower() for s in SECTIONS] assert any("user stor" in t for t in section_titles), \ f"SECTIONS must include 'User Stories' but got: {section_titles}" def test_sections_include_functional_requirements(self): """SECTIONS must include a Functional Requirements section.""" from app.services.workspace_service import SECTIONS section_titles = [s["title"].lower() for s in SECTIONS] assert any("functional requirement" in t for t in section_titles), \ f"SECTIONS must include 'Functional Requirements' but got: {section_titles}" def test_sections_include_non_functional_requirements(self): """SECTIONS must include a Non-Functional Requirements section.""" from app.services.workspace_service import SECTIONS section_titles = [s["title"].lower() for s in SECTIONS] assert any("non-functional" in t or "non functional" in t for t in section_titles), \ f"SECTIONS must include 'Non-Functional Requirements' but got: {section_titles}" def test_sections_include_use_cases(self): """SECTIONS must include a Use Cases section.""" from app.services.workspace_service import SECTIONS section_titles = [s["title"].lower() for s in SECTIONS] assert any("use case" in t for t in section_titles), \ f"SECTIONS must include 'Use Cases' but got: {section_titles}" class TestEvaluateEndpointSchema: """Verify the evaluate endpoint returns correct response schema.""" def test_evaluate_returns_correct_structure(self): from app.main import app client = TestClient(app) response = client.post("/api/workspace/evaluate", json={ "idea": "Build a task management app with AI-powered prioritization" }) assert response.status_code == 200 data = response.json() assert "scores" in data assert "overall_score" in data assert "threshold_met" in data assert "weak_dimensions" in data assert "targeted_questions" in data def test_evaluate_empty_idea_returns_fallback(self): from app.main import app client = TestClient(app) response = client.post("/api/workspace/evaluate", json={"idea": ""}) assert response.status_code == 200 data = response.json() assert data["overall_score"] == 0.0 assert data["threshold_met"] is False assert len(data["targeted_questions"]) > 0 class TestDirectionsEndpointSchema: """Verify the directions endpoint returns correct response schema.""" def test_directions_returns_correct_structure(self): from app.main import app client = TestClient(app) response = client.post("/api/workspace/directions", json={ "answers": {"q1": "B2C", "q2": "Web app", "q3": "Task management", "q4": "AI-powered prioritization"} }) assert response.status_code == 200 data = response.json() assert "directions" in data assert isinstance(data["directions"], list) if len(data["directions"]) > 0: direction = data["directions"][0] assert "id" in direction assert "title" in direction assert "description" in direction assert "tags" in direction def test_directions_empty_answers_returns_fallback(self): from app.main import app client = TestClient(app) response = client.post("/api/workspace/directions", json={"answers": {}}) assert response.status_code == 200 data = response.json() assert len(data["directions"]) >= 2 class TestRefineEndpoint: """Verify the refine endpoint handles edge cases gracefully.""" def test_refine_empty_content_returns_original(self): from app.main import app client = TestClient(app) response = client.post("/api/workspace/refine", json={ "section_id": "sec-test", "content": "", "prompt": "Make it better" }) assert response.status_code == 200 data = response.json() assert data["section_id"] == "sec-test" assert data["content"] == "" class TestPipelineGenerationPath: """Guards the path /api/workspace/generate actually takes. `SECTIONS` and the tests above now describe only the degraded single-agent fallback, used when the orchestrator cannot start. Workspace generation normally runs the full agent team, and nothing above would notice if that mapping broke - these cover it. """ def test_section_ids_round_trip_to_roles(self): """Every agent's section id must map back to that agent. `save_workspace` uses this inverse to decide which sections are agent output; if it stops round-tripping, saved projects silently lose every agent and the dashboard reports "No output". """ from app.services.workspace_service import ( _role_section_id, pipeline_section_order, role_for_section_id, ) for role in pipeline_section_order(): assert role_for_section_id(_role_section_id(role)) == role def test_legacy_section_ids_are_not_mistaken_for_roles(self): """Fallback section ids must not be read as agent output.""" from app.services.workspace_service import SECTIONS, role_for_section_id for section in SECTIONS: assert role_for_section_id(section["id"]) is None def test_pipeline_order_matches_the_orchestrator(self): """The section order must be derived from the phases, not restated.""" from app.core.pipeline_orchestrator import PipelineOrchestrator from app.services.workspace_service import pipeline_section_order expected = [ role.value for role in PipelineOrchestrator.PHASE_1 + PipelineOrchestrator.PHASE_2 + PipelineOrchestrator.PHASE_3 ] assert pipeline_section_order() == expected assert len(expected) == 11 def test_titles_render_acronyms(self): """`.title()` alone produces "Ux Designer" and "Qa Strategist".""" from app.services.workspace_service import _role_section_title assert _role_section_title("ux_designer") == "UX Designer" assert _role_section_title("api_designer") == "API Designer" assert _role_section_title("qa_strategist") == "QA Strategist" assert _role_section_title("devops_architect") == "DevOps Architect" assert _role_section_title("solution_architect") == "Solution Architect" def test_save_puts_agent_output_at_top_level_and_metadata_under_underscore(self): """The dashboard reads `artifacts` as a role -> markdown map. Writing workspace metadata at the top level made a saved spec report "4 Agents" and render chips reading "type" and "brief". """ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.core.database import Base from app.core import models from app.services.workspace_service import _role_section_id, save_workspace # Its own in-memory engine - never the shared one, whose teardown is # destructive elsewhere in this suite. engine = create_engine("sqlite://") Base.metadata.create_all(engine) db = sessionmaker(bind=engine)() try: user = models.User( id=1, google_id="g", email="w@example.com", full_name="W", role="user" ) db.add(user) db.commit() sections = [ { "id": _role_section_id("solution_architect"), "title": "Solution Architect", "content": "## Architecture\nContent.", "order": 0, }, { "id": "sec-overview", "title": "Project Overview", "content": "Fallback section.", "order": 1, }, ] project = save_workspace( db=db, current_user=user, title="T", direction_id="dir-a", brief="b", sections=sections, ) assert project.artifacts["solution_architect"] == "## Architecture\nContent." assert "_workspace" in project.artifacts assert len(project.artifacts["_workspace"]["sections"]) == 2 # Metadata must not sit alongside roles, or it gets counted as one. for key in ("type", "direction_id", "brief", "sections"): assert key not in project.artifacts # The fallback section has no role, so it is not agent output. roles = [k for k in project.artifacts if not k.startswith("_")] assert roles == ["solution_architect"] finally: db.close() engine.dispose()