Spaces:
Sleeping
Sleeping
| """ | |
| 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() | |