ruslanmv commited on
Commit
bbda63d
·
verified ·
1 Parent(s): 2c304fc

fix: escape curly braces in chatbot template to prevent KeyError with .format()

Browse files
.pytest_cache/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Created by pytest automatically.
2
+ *
.pytest_cache/CACHEDIR.TAG ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag created by pytest.
3
+ # For information about cache directory tags, see:
4
+ # https://bford.info/cachedir/spec.html
.pytest_cache/README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # pytest cache directory #
2
+
3
+ This directory contains data from the pytest's cache plugin,
4
+ which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
5
+
6
+ **Do not** commit this to version control.
7
+
8
+ See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
.pytest_cache/v/cache/lastfailed ADDED
@@ -0,0 +1 @@
 
 
1
+ {}
.pytest_cache/v/cache/nodeids ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ "tests/test_app.py::TestAppPlanner::test_app_name_contains_meaningful_words",
3
+ "tests/test_app.py::TestAppPlanner::test_app_name_falls_back_when_only_stop_words",
4
+ "tests/test_app.py::TestAppPlanner::test_app_name_is_slugified",
5
+ "tests/test_app.py::TestAppPlanner::test_chatbot_matches_chatbot_template",
6
+ "tests/test_app.py::TestAppPlanner::test_plan_components_is_list",
7
+ "tests/test_app.py::TestAppPlanner::test_plan_has_required_key[app_name]",
8
+ "tests/test_app.py::TestAppPlanner::test_plan_has_required_key[app_type]",
9
+ "tests/test_app.py::TestAppPlanner::test_plan_has_required_key[components]",
10
+ "tests/test_app.py::TestAppPlanner::test_plan_has_required_key[description]",
11
+ "tests/test_app.py::TestAppPlanner::test_plan_has_required_key[model_task]",
12
+ "tests/test_app.py::TestAppPlanner::test_plan_has_required_key[original_prompt]",
13
+ "tests/test_app.py::TestAppPlanner::test_plan_has_required_key[sdk]",
14
+ "tests/test_app.py::TestAppPlanner::test_plan_has_required_key[template_key]",
15
+ "tests/test_app.py::TestAppPlanner::test_plan_has_required_key[title]",
16
+ "tests/test_app.py::TestAppPlanner::test_portfolio_matches_template",
17
+ "tests/test_app.py::TestAppPlanner::test_rest_api_matches_template",
18
+ "tests/test_app.py::TestAppPlanner::test_sdk_auto_selects_docker_for_api_service",
19
+ "tests/test_app.py::TestAppPlanner::test_sdk_auto_selects_gradio_for_chatbot",
20
+ "tests/test_app.py::TestAppPlanner::test_sdk_auto_selects_gradio_for_image_classifier",
21
+ "tests/test_app.py::TestAppPlanner::test_sdk_auto_selects_static_for_landing_page",
22
+ "tests/test_app.py::TestAppPlanner::test_sdk_defaults_to_gradio_when_no_signal",
23
+ "tests/test_app.py::TestAppPlanner::test_sdk_preference_overrides_auto",
24
+ "tests/test_app.py::TestAppPlanner::test_sentiment_matches_template",
25
+ "tests/test_app.py::TestAppPlanner::test_summarizer_matches_template",
26
+ "tests/test_app.py::TestCodeChecker::test_dangerous_pattern_warned",
27
+ "tests/test_app.py::TestCodeChecker::test_docker_missing_dockerfile_is_error",
28
+ "tests/test_app.py::TestCodeChecker::test_dockerfile_missing_cmd_is_error",
29
+ "tests/test_app.py::TestCodeChecker::test_dockerfile_missing_expose_is_warning",
30
+ "tests/test_app.py::TestCodeChecker::test_dockerfile_missing_from_is_error",
31
+ "tests/test_app.py::TestCodeChecker::test_empty_file_flagged",
32
+ "tests/test_app.py::TestCodeChecker::test_eval_warned",
33
+ "tests/test_app.py::TestCodeChecker::test_gradio_missing_app_py_is_error",
34
+ "tests/test_app.py::TestCodeChecker::test_html_missing_tags_warned",
35
+ "tests/test_app.py::TestCodeChecker::test_invalid_python_syntax_caught",
36
+ "tests/test_app.py::TestCodeChecker::test_overall_valid_when_no_errors",
37
+ "tests/test_app.py::TestCodeChecker::test_readme_missing_frontmatter_warned",
38
+ "tests/test_app.py::TestCodeChecker::test_static_missing_index_html_is_error",
39
+ "tests/test_app.py::TestCodeChecker::test_valid_dockerfile_passes",
40
+ "tests/test_app.py::TestCodeChecker::test_valid_html_passes",
41
+ "tests/test_app.py::TestCodeChecker::test_valid_python_passes",
42
+ "tests/test_app.py::TestDockerGenerator::test_app_py_is_valid_python",
43
+ "tests/test_app.py::TestDockerGenerator::test_docker_template_app_py_is_valid_python[generic_docker]",
44
+ "tests/test_app.py::TestDockerGenerator::test_docker_template_app_py_is_valid_python[rest_api]",
45
+ "tests/test_app.py::TestDockerGenerator::test_dockerfile_exposes_7860",
46
+ "tests/test_app.py::TestDockerGenerator::test_dockerfile_has_cmd",
47
+ "tests/test_app.py::TestDockerGenerator::test_dockerfile_has_from",
48
+ "tests/test_app.py::TestDockerGenerator::test_generate_produces_app_py",
49
+ "tests/test_app.py::TestDockerGenerator::test_generate_produces_dockerfile",
50
+ "tests/test_app.py::TestDockerGenerator::test_generate_produces_requirements",
51
+ "tests/test_app.py::TestDockerGenerator::test_generic_docker_fallback",
52
+ "tests/test_app.py::TestDockerGenerator::test_parse_files_empty_returns_empty",
53
+ "tests/test_app.py::TestDockerGenerator::test_parse_files_marker_format",
54
+ "tests/test_app.py::TestEndToEnd::test_chatbot_e2e",
55
+ "tests/test_app.py::TestEndToEnd::test_image_classifier_e2e",
56
+ "tests/test_app.py::TestEndToEnd::test_portfolio_e2e",
57
+ "tests/test_app.py::TestEndToEnd::test_rest_api_e2e",
58
+ "tests/test_app.py::TestEndToEnd::test_summarizer_small_model_e2e",
59
+ "tests/test_app.py::TestFastAPIApp::test_download_nonexistent_project_returns_404",
60
+ "tests/test_app.py::TestFastAPIApp::test_edit_nonexistent_project_returns_404",
61
+ "tests/test_app.py::TestFastAPIApp::test_get_file_nonexistent_project_returns_404",
62
+ "tests/test_app.py::TestFastAPIApp::test_home_contains_title",
63
+ "tests/test_app.py::TestFastAPIApp::test_home_returns_200",
64
+ "tests/test_app.py::TestFastAPIApp::test_post_generate_contains_file_content",
65
+ "tests/test_app.py::TestFastAPIApp::test_post_generate_returns_html",
66
+ "tests/test_app.py::TestGradioGenerator::test_extract_code_from_markdown_block",
67
+ "tests/test_app.py::TestGradioGenerator::test_extract_code_plain_python",
68
+ "tests/test_app.py::TestGradioGenerator::test_fallback_template_produces_valid_python[chatbot]",
69
+ "tests/test_app.py::TestGradioGenerator::test_fallback_template_produces_valid_python[image_classifier]",
70
+ "tests/test_app.py::TestGradioGenerator::test_fallback_template_produces_valid_python[question_answering]",
71
+ "tests/test_app.py::TestGradioGenerator::test_fallback_template_produces_valid_python[sentiment_analyzer]",
72
+ "tests/test_app.py::TestGradioGenerator::test_fallback_template_produces_valid_python[text_generator]",
73
+ "tests/test_app.py::TestGradioGenerator::test_fallback_template_produces_valid_python[text_summarizer]",
74
+ "tests/test_app.py::TestGradioGenerator::test_fallback_template_produces_valid_python[translator]",
75
+ "tests/test_app.py::TestGradioGenerator::test_generate_produces_valid_python_chatbot",
76
+ "tests/test_app.py::TestGradioGenerator::test_generate_produces_valid_python_image_classifier",
77
+ "tests/test_app.py::TestGradioGenerator::test_generate_produces_valid_python_qa",
78
+ "tests/test_app.py::TestGradioGenerator::test_generate_produces_valid_python_sentiment",
79
+ "tests/test_app.py::TestGradioGenerator::test_generate_produces_valid_python_summarizer",
80
+ "tests/test_app.py::TestGradioGenerator::test_generate_produces_valid_python_text_gen",
81
+ "tests/test_app.py::TestGradioGenerator::test_generate_produces_valid_python_translator",
82
+ "tests/test_app.py::TestGradioGenerator::test_generated_code_contains_gradio_import",
83
+ "tests/test_app.py::TestGradioGenerator::test_generated_code_contains_launch",
84
+ "tests/test_app.py::TestGradioGenerator::test_generic_fallback_for_unknown_template",
85
+ "tests/test_app.py::TestModelRecommender::test_catalog_task_has_all_sizes[automatic-speech-recognition]",
86
+ "tests/test_app.py::TestModelRecommender::test_catalog_task_has_all_sizes[image-classification]",
87
+ "tests/test_app.py::TestModelRecommender::test_catalog_task_has_all_sizes[object-detection]",
88
+ "tests/test_app.py::TestModelRecommender::test_catalog_task_has_all_sizes[question-answering]",
89
+ "tests/test_app.py::TestModelRecommender::test_catalog_task_has_all_sizes[summarization]",
90
+ "tests/test_app.py::TestModelRecommender::test_catalog_task_has_all_sizes[text-classification]",
91
+ "tests/test_app.py::TestModelRecommender::test_catalog_task_has_all_sizes[text-generation]",
92
+ "tests/test_app.py::TestModelRecommender::test_catalog_task_has_all_sizes[text-to-image]",
93
+ "tests/test_app.py::TestModelRecommender::test_catalog_task_has_all_sizes[token-classification]",
94
+ "tests/test_app.py::TestModelRecommender::test_catalog_task_has_all_sizes[translation]",
95
+ "tests/test_app.py::TestModelRecommender::test_different_tasks_produce_different_models",
96
+ "tests/test_app.py::TestModelRecommender::test_get_primary_model_none_for_missing_task",
97
+ "tests/test_app.py::TestModelRecommender::test_get_primary_model_returns_string",
98
+ "tests/test_app.py::TestModelRecommender::test_invalid_size_falls_back_to_medium",
99
+ "tests/test_app.py::TestModelRecommender::test_large_models_recommend_gpu",
100
+ "tests/test_app.py::TestModelRecommender::test_large_size_returns_models",
101
+ "tests/test_app.py::TestModelRecommender::test_no_task_returns_empty",
102
+ "tests/test_app.py::TestModelRecommender::test_recommend_returns_list",
103
+ "tests/test_app.py::TestModelRecommender::test_recommended_models_have_id",
104
+ "tests/test_app.py::TestModelRecommender::test_recommended_models_have_required_fields",
105
+ "tests/test_app.py::TestModelRecommender::test_small_and_large_return_different_models",
106
+ "tests/test_app.py::TestModelRecommender::test_small_size_returns_models",
107
+ "tests/test_app.py::TestModelRecommender::test_summarization_vs_translation",
108
+ "tests/test_app.py::TestReadmeGenerator::test_docker_readme_mentions_fastapi",
109
+ "tests/test_app.py::TestReadmeGenerator::test_frontmatter_contains_sdk_docker",
110
+ "tests/test_app.py::TestReadmeGenerator::test_frontmatter_contains_sdk_gradio",
111
+ "tests/test_app.py::TestReadmeGenerator::test_frontmatter_contains_sdk_static",
112
+ "tests/test_app.py::TestReadmeGenerator::test_frontmatter_contains_title",
113
+ "tests/test_app.py::TestReadmeGenerator::test_readme_contains_app_name_in_body",
114
+ "tests/test_app.py::TestReadmeGenerator::test_readme_contains_description",
115
+ "tests/test_app.py::TestReadmeGenerator::test_readme_contains_features_section",
116
+ "tests/test_app.py::TestReadmeGenerator::test_readme_contains_model_reference",
117
+ "tests/test_app.py::TestReadmeGenerator::test_readme_contains_tech_stack",
118
+ "tests/test_app.py::TestReadmeGenerator::test_readme_has_yaml_frontmatter",
119
+ "tests/test_app.py::TestReadmeGenerator::test_static_readme_mentions_html",
120
+ "tests/test_app.py::TestRepoGenerator::test_docker_repo_has_app_py",
121
+ "tests/test_app.py::TestRepoGenerator::test_docker_repo_has_dockerfile",
122
+ "tests/test_app.py::TestRepoGenerator::test_docker_repo_has_readme",
123
+ "tests/test_app.py::TestRepoGenerator::test_docker_repo_has_requirements",
124
+ "tests/test_app.py::TestRepoGenerator::test_gradio_chart_component_includes_matplotlib",
125
+ "tests/test_app.py::TestRepoGenerator::test_gradio_image_task_includes_pillow",
126
+ "tests/test_app.py::TestRepoGenerator::test_gradio_repo_has_app_py",
127
+ "tests/test_app.py::TestRepoGenerator::test_gradio_repo_has_gitignore",
128
+ "tests/test_app.py::TestRepoGenerator::test_gradio_repo_has_readme",
129
+ "tests/test_app.py::TestRepoGenerator::test_gradio_repo_has_requirements_txt",
130
+ "tests/test_app.py::TestRepoGenerator::test_static_index_contains_title",
131
+ "tests/test_app.py::TestRepoGenerator::test_static_repo_has_index_html",
132
+ "tests/test_app.py::TestRepoGenerator::test_static_repo_has_readme",
133
+ "tests/test_app.py::TestRepoGenerator::test_static_repo_has_style_css"
134
+ ]
app/codegen/gradio_generator.py CHANGED
@@ -18,13 +18,13 @@ from huggingface_hub import InferenceClient
18
  client = InferenceClient("{model_id}")
19
 
20
  def respond(message, history, system_message, max_tokens, temperature, top_p):
21
- messages = [{"role": "system", "content": system_message}]
22
  for user_msg, bot_msg in history:
23
  if user_msg:
24
- messages.append({"role": "user", "content": user_msg})
25
  if bot_msg:
26
- messages.append({"role": "assistant", "content": bot_msg})
27
- messages.append({"role": "user", "content": message})
28
 
29
  response = ""
30
  for chunk in client.chat_completion(
 
18
  client = InferenceClient("{model_id}")
19
 
20
  def respond(message, history, system_message, max_tokens, temperature, top_p):
21
+ messages = [{{"role": "system", "content": system_message}}]
22
  for user_msg, bot_msg in history:
23
  if user_msg:
24
+ messages.append({{"role": "user", "content": user_msg}})
25
  if bot_msg:
26
+ messages.append({{"role": "assistant", "content": bot_msg}})
27
+ messages.append({{"role": "user", "content": message}})
28
 
29
  response = ""
30
  for chunk in client.chat_completion(
tests/__init__.py ADDED
File without changes
tests/conftest.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared fixtures for AutoApp Builder tests."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ # Ensure imports resolve from the hf/ directory.
9
+ HF_ROOT = Path(__file__).resolve().parent.parent
10
+ if str(HF_ROOT) not in sys.path:
11
+ sys.path.insert(0, str(HF_ROOT))
12
+
13
+ from app.main import app as fastapi_app
14
+
15
+
16
+ @pytest.fixture()
17
+ def client():
18
+ """Synchronous test client for the FastAPI app using Starlette's TestClient."""
19
+ from starlette.testclient import TestClient
20
+
21
+ with TestClient(fastapi_app, raise_server_exceptions=False) as c:
22
+ yield c
tests/test_app.py ADDED
@@ -0,0 +1,901 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Comprehensive unit tests for the AutoApp Builder Hugging Face Space.
3
+
4
+ All tests run offline -- no external API calls are made.
5
+ """
6
+
7
+ import ast
8
+ import sys
9
+ from pathlib import Path
10
+ from unittest.mock import patch, MagicMock
11
+
12
+ import pytest
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Path setup -- ensure ``app`` package is importable from the hf/ directory.
16
+ # ---------------------------------------------------------------------------
17
+ HF_ROOT = Path(__file__).resolve().parent.parent
18
+ if str(HF_ROOT) not in sys.path:
19
+ sys.path.insert(0, str(HF_ROOT))
20
+
21
+ from app.engine.app_planner import AppPlanner, APP_TEMPLATES
22
+ from app.engine.model_recommender import ModelRecommender, MODEL_CATALOG
23
+ from app.codegen.repo_generator import RepoGenerator
24
+ from app.codegen.gradio_generator import GradioGenerator, GRADIO_TEMPLATES
25
+ from app.codegen.docker_generator import DockerGenerator, DOCKER_TEMPLATES
26
+ from app.codegen.readme_generator import ReadmeGenerator
27
+ from app.validators.code_checker import CodeChecker
28
+
29
+
30
+ def _can_run_fastapi_tests():
31
+ """Check if FastAPI integration tests can run in this environment."""
32
+ try:
33
+ from starlette.testclient import TestClient
34
+ from app.main import app
35
+ with TestClient(app, raise_server_exceptions=False) as c:
36
+ resp = c.get("/")
37
+ return resp.status_code != 500
38
+ except Exception:
39
+ return False
40
+
41
+
42
+ # ===================================================================
43
+ # 1. AppPlanner
44
+ # ===================================================================
45
+
46
+ class TestAppPlanner:
47
+ """Tests for AppPlanner.analyze()."""
48
+
49
+ def setup_method(self):
50
+ self.planner = AppPlanner()
51
+
52
+ # -- SDK auto-selection ------------------------------------------------
53
+
54
+ def test_sdk_auto_selects_gradio_for_chatbot(self):
55
+ plan = self.planner.analyze("Build a chatbot that answers questions", "auto")
56
+ assert plan["sdk"] == "gradio"
57
+
58
+ def test_sdk_auto_selects_gradio_for_image_classifier(self):
59
+ plan = self.planner.analyze(
60
+ "Build a Gradio app that classifies images using ResNet", "auto"
61
+ )
62
+ assert plan["sdk"] == "gradio"
63
+
64
+ def test_sdk_auto_selects_docker_for_api_service(self):
65
+ plan = self.planner.analyze(
66
+ "Build a REST API service with FastAPI endpoints", "auto"
67
+ )
68
+ assert plan["sdk"] == "docker"
69
+
70
+ def test_sdk_auto_selects_static_for_landing_page(self):
71
+ plan = self.planner.analyze(
72
+ "Create a beautiful static landing page for my portfolio", "auto"
73
+ )
74
+ assert plan["sdk"] == "static"
75
+
76
+ def test_sdk_preference_overrides_auto(self):
77
+ plan = self.planner.analyze("Build a chatbot", "docker")
78
+ assert plan["sdk"] == "docker"
79
+
80
+ def test_sdk_defaults_to_gradio_when_no_signal(self):
81
+ plan = self.planner.analyze("Do something cool", "auto")
82
+ assert plan["sdk"] == "gradio"
83
+
84
+ # -- App name generation (slugified) -----------------------------------
85
+
86
+ def test_app_name_is_slugified(self):
87
+ plan = self.planner.analyze("Build an Image Classifier for dogs", "auto")
88
+ name = plan["app_name"]
89
+ assert " " not in name
90
+ assert name == name.lower()
91
+ # Should contain only lowercase alphanumeric and hyphens
92
+ assert all(c.isalnum() or c == "-" for c in name)
93
+
94
+ def test_app_name_contains_meaningful_words(self):
95
+ plan = self.planner.analyze("Create a sentiment analysis dashboard", "auto")
96
+ name = plan["app_name"]
97
+ # Should contain at least one meaningful keyword
98
+ assert any(kw in name for kw in ["sentiment", "analysis", "dashboard"])
99
+
100
+ def test_app_name_falls_back_when_only_stop_words(self):
101
+ plan = self.planner.analyze("Build a the an is are", "auto")
102
+ name = plan["app_name"]
103
+ # Should still produce a valid slug (fallback to "my-app")
104
+ assert len(name) > 0
105
+ assert "-" in name or name.isalnum()
106
+
107
+ # -- Plan structure has required keys ----------------------------------
108
+
109
+ REQUIRED_KEYS = [
110
+ "sdk", "app_name", "app_type", "title", "description",
111
+ "components", "model_task", "template_key", "original_prompt",
112
+ ]
113
+
114
+ @pytest.mark.parametrize("key", REQUIRED_KEYS)
115
+ def test_plan_has_required_key(self, key):
116
+ plan = self.planner.analyze("Build a chatbot", "auto")
117
+ assert key in plan, f"Plan missing required key: {key}"
118
+
119
+ def test_plan_components_is_list(self):
120
+ plan = self.planner.analyze("Build a chatbot", "auto")
121
+ assert isinstance(plan["components"], list)
122
+ assert len(plan["components"]) > 0
123
+
124
+ # -- Template matching -------------------------------------------------
125
+
126
+ def test_chatbot_matches_chatbot_template(self):
127
+ plan = self.planner.analyze("Build a chatbot", "auto")
128
+ assert plan["template_key"] == "chatbot"
129
+
130
+ def test_summarizer_matches_template(self):
131
+ plan = self.planner.analyze("Build a text summarization tool", "auto")
132
+ assert plan["template_key"] == "text_summarizer"
133
+
134
+ def test_sentiment_matches_template(self):
135
+ plan = self.planner.analyze("Create a sentiment analysis app", "auto")
136
+ assert plan["template_key"] == "sentiment_analyzer"
137
+
138
+ def test_portfolio_matches_template(self):
139
+ plan = self.planner.analyze("Create a portfolio website to showcase my work", "auto")
140
+ assert plan["template_key"] == "portfolio"
141
+
142
+ def test_rest_api_matches_template(self):
143
+ plan = self.planner.analyze("Build a REST API server with FastAPI", "auto")
144
+ assert plan["template_key"] == "rest_api"
145
+
146
+
147
+ # ===================================================================
148
+ # 2. ModelRecommender
149
+ # ===================================================================
150
+
151
+ class TestModelRecommender:
152
+ """Tests for ModelRecommender."""
153
+
154
+ def setup_method(self):
155
+ self.recommender = ModelRecommender()
156
+
157
+ def _make_plan(self, task: str) -> dict:
158
+ return {"model_task": task}
159
+
160
+ # -- Recommendations return valid model IDs ----------------------------
161
+
162
+ def test_recommend_returns_list(self):
163
+ models = self.recommender.recommend(self._make_plan("text-generation"))
164
+ assert isinstance(models, list)
165
+ assert len(models) > 0
166
+
167
+ def test_recommended_models_have_id(self):
168
+ models = self.recommender.recommend(self._make_plan("text-generation"))
169
+ for m in models:
170
+ assert "id" in m
171
+ assert isinstance(m["id"], str)
172
+ assert len(m["id"]) > 0
173
+
174
+ def test_recommended_models_have_required_fields(self):
175
+ models = self.recommender.recommend(self._make_plan("summarization"))
176
+ for m in models:
177
+ assert "id" in m
178
+ assert "desc" in m
179
+ assert "size" in m
180
+ assert "gpu_recommended" in m
181
+
182
+ # -- Different task types produce different models ---------------------
183
+
184
+ def test_different_tasks_produce_different_models(self):
185
+ text_gen = self.recommender.recommend(self._make_plan("text-generation"))
186
+ img_cls = self.recommender.recommend(self._make_plan("image-classification"))
187
+ assert text_gen[0]["id"] != img_cls[0]["id"]
188
+
189
+ def test_summarization_vs_translation(self):
190
+ summ = self.recommender.recommend(self._make_plan("summarization"))
191
+ trans = self.recommender.recommend(self._make_plan("translation"))
192
+ assert summ[0]["id"] != trans[0]["id"]
193
+
194
+ # -- Model size filtering (small/medium/large) -------------------------
195
+
196
+ def test_small_size_returns_models(self):
197
+ models = self.recommender.recommend(
198
+ self._make_plan("text-generation"), model_size="small"
199
+ )
200
+ assert len(models) > 0
201
+
202
+ def test_large_size_returns_models(self):
203
+ models = self.recommender.recommend(
204
+ self._make_plan("text-generation"), model_size="large"
205
+ )
206
+ assert len(models) > 0
207
+
208
+ def test_small_and_large_return_different_models(self):
209
+ small = self.recommender.recommend(
210
+ self._make_plan("text-generation"), model_size="small"
211
+ )
212
+ large = self.recommender.recommend(
213
+ self._make_plan("text-generation"), model_size="large"
214
+ )
215
+ assert small[0]["id"] != large[0]["id"]
216
+
217
+ def test_invalid_size_falls_back_to_medium(self):
218
+ models = self.recommender.recommend(
219
+ self._make_plan("text-generation"), model_size="xxl"
220
+ )
221
+ medium = self.recommender.recommend(
222
+ self._make_plan("text-generation"), model_size="medium"
223
+ )
224
+ # Invalid size normalises to medium, so results should match
225
+ assert models[0]["id"] == medium[0]["id"]
226
+
227
+ def test_large_models_recommend_gpu(self):
228
+ models = self.recommender.recommend(
229
+ self._make_plan("text-generation"), model_size="large"
230
+ )
231
+ assert models[0]["gpu_recommended"] is True
232
+
233
+ def test_no_task_returns_empty(self):
234
+ models = self.recommender.recommend({"model_task": None})
235
+ assert models == []
236
+
237
+ # -- get_primary_model -------------------------------------------------
238
+
239
+ def test_get_primary_model_returns_string(self):
240
+ model_id = self.recommender.get_primary_model(
241
+ self._make_plan("text-generation")
242
+ )
243
+ assert isinstance(model_id, str)
244
+ assert "/" in model_id or model_id.startswith("models?")
245
+
246
+ def test_get_primary_model_none_for_missing_task(self):
247
+ model_id = self.recommender.get_primary_model({"model_task": None})
248
+ assert model_id is None
249
+
250
+ # -- All catalog tasks have all sizes ----------------------------------
251
+
252
+ @pytest.mark.parametrize("task", list(MODEL_CATALOG.keys()))
253
+ def test_catalog_task_has_all_sizes(self, task):
254
+ for size in ("small", "medium", "large"):
255
+ assert size in MODEL_CATALOG[task], (
256
+ f"MODEL_CATALOG['{task}'] missing size '{size}'"
257
+ )
258
+
259
+
260
+ # ===================================================================
261
+ # 3. RepoGenerator
262
+ # ===================================================================
263
+
264
+ class TestRepoGenerator:
265
+ """Tests for RepoGenerator (uses template fallbacks, no API)."""
266
+
267
+ def setup_method(self):
268
+ self.generator = RepoGenerator()
269
+
270
+ def _make_plan(self, sdk, template_key=None, **overrides):
271
+ plan = {
272
+ "sdk": sdk,
273
+ "app_name": "test-app",
274
+ "app_type": template_key or "custom",
275
+ "title": "Test App",
276
+ "description": "A test application",
277
+ "components": ["text_input", "text_output"],
278
+ "model_task": "text-generation",
279
+ "template_key": template_key,
280
+ "original_prompt": "build a test app",
281
+ "recommended_models": [
282
+ {"id": "Qwen/Qwen2.5-7B-Instruct", "desc": "Test", "size": "7B"}
283
+ ],
284
+ "extra_features": [],
285
+ }
286
+ plan.update(overrides)
287
+ return plan
288
+
289
+ # -- Gradio repo generation -------------------------------------------
290
+
291
+ def test_gradio_repo_has_app_py(self):
292
+ plan = self._make_plan("gradio", "chatbot")
293
+ files = self.generator.generate(plan, "build a chatbot")
294
+ assert "app.py" in files
295
+
296
+ def test_gradio_repo_has_requirements_txt(self):
297
+ plan = self._make_plan("gradio", "chatbot")
298
+ files = self.generator.generate(plan, "build a chatbot")
299
+ assert "requirements.txt" in files
300
+ assert "gradio" in files["requirements.txt"]
301
+
302
+ def test_gradio_repo_has_readme(self):
303
+ plan = self._make_plan("gradio", "chatbot")
304
+ files = self.generator.generate(plan, "build a chatbot")
305
+ assert "README.md" in files
306
+
307
+ def test_gradio_repo_has_gitignore(self):
308
+ plan = self._make_plan("gradio", "chatbot")
309
+ files = self.generator.generate(plan, "build a chatbot")
310
+ assert ".gitignore" in files
311
+
312
+ # -- Docker repo generation -------------------------------------------
313
+
314
+ def test_docker_repo_has_dockerfile(self):
315
+ plan = self._make_plan("docker", "rest_api")
316
+ files = self.generator.generate(plan, "build a rest api")
317
+ assert "Dockerfile" in files
318
+
319
+ def test_docker_repo_has_app_py(self):
320
+ plan = self._make_plan("docker", "rest_api")
321
+ files = self.generator.generate(plan, "build a rest api")
322
+ assert "app.py" in files
323
+
324
+ def test_docker_repo_has_requirements(self):
325
+ plan = self._make_plan("docker", "rest_api")
326
+ files = self.generator.generate(plan, "build a rest api")
327
+ assert "requirements.txt" in files
328
+
329
+ def test_docker_repo_has_readme(self):
330
+ plan = self._make_plan("docker", "rest_api")
331
+ files = self.generator.generate(plan, "build a rest api")
332
+ assert "README.md" in files
333
+
334
+ # -- Static repo generation -------------------------------------------
335
+
336
+ def test_static_repo_has_index_html(self):
337
+ plan = self._make_plan("static", "portfolio", model_task=None)
338
+ files = self.generator.generate(plan, "build a portfolio site")
339
+ assert "index.html" in files
340
+
341
+ def test_static_repo_has_style_css(self):
342
+ plan = self._make_plan("static", "portfolio", model_task=None)
343
+ files = self.generator.generate(plan, "build a portfolio site")
344
+ assert "style.css" in files
345
+
346
+ def test_static_repo_has_readme(self):
347
+ plan = self._make_plan("static", "portfolio", model_task=None)
348
+ files = self.generator.generate(plan, "build a portfolio site")
349
+ assert "README.md" in files
350
+
351
+ def test_static_index_contains_title(self):
352
+ plan = self._make_plan("static", "portfolio", title="My Portfolio", model_task=None)
353
+ files = self.generator.generate(plan, "portfolio site")
354
+ assert "My Portfolio" in files["index.html"]
355
+
356
+ # -- Gradio requirements include task-specific deps --------------------
357
+
358
+ def test_gradio_image_task_includes_pillow(self):
359
+ plan = self._make_plan("gradio", "image_classifier", model_task="image-classification")
360
+ files = self.generator.generate(plan, "image classifier")
361
+ assert "Pillow" in files["requirements.txt"]
362
+
363
+ def test_gradio_chart_component_includes_matplotlib(self):
364
+ plan = self._make_plan(
365
+ "gradio", "sentiment_analyzer",
366
+ model_task="text-classification",
367
+ components=["text_input", "chart_output"],
368
+ )
369
+ files = self.generator.generate(plan, "sentiment dashboard")
370
+ assert "matplotlib" in files["requirements.txt"]
371
+
372
+
373
+ # ===================================================================
374
+ # 4. GradioGenerator
375
+ # ===================================================================
376
+
377
+ class TestGradioGenerator:
378
+ """Tests for GradioGenerator (template fallback, no LLM calls)."""
379
+
380
+ def setup_method(self):
381
+ self.gen = GradioGenerator()
382
+
383
+ def _make_plan(self, template_key, model_task="text-generation"):
384
+ return {
385
+ "template_key": template_key,
386
+ "title": "Test App",
387
+ "description": "A test",
388
+ "model_task": model_task,
389
+ "components": [],
390
+ "recommended_models": [
391
+ {"id": "Qwen/Qwen2.5-7B-Instruct", "desc": "Test", "size": "7B"}
392
+ ],
393
+ "extra_features": [],
394
+ }
395
+
396
+ def test_generate_produces_valid_python_chatbot(self):
397
+ plan = self._make_plan("chatbot")
398
+ code = self.gen.generate(plan, "build a chatbot")
399
+ # Must parse without syntax errors
400
+ ast.parse(code)
401
+
402
+ def test_generate_produces_valid_python_image_classifier(self):
403
+ plan = self._make_plan("image_classifier", "image-classification")
404
+ code = self.gen.generate(plan, "image classifier")
405
+ ast.parse(code)
406
+
407
+ def test_generate_produces_valid_python_summarizer(self):
408
+ plan = self._make_plan("text_summarizer", "summarization")
409
+ code = self.gen.generate(plan, "text summarizer")
410
+ ast.parse(code)
411
+
412
+ def test_generate_produces_valid_python_sentiment(self):
413
+ plan = self._make_plan("sentiment_analyzer", "text-classification")
414
+ code = self.gen.generate(plan, "sentiment tool")
415
+ ast.parse(code)
416
+
417
+ def test_generate_produces_valid_python_translator(self):
418
+ plan = self._make_plan("translator", "translation")
419
+ code = self.gen.generate(plan, "translator")
420
+ ast.parse(code)
421
+
422
+ def test_generate_produces_valid_python_qa(self):
423
+ plan = self._make_plan("question_answering", "question-answering")
424
+ code = self.gen.generate(plan, "question answering")
425
+ ast.parse(code)
426
+
427
+ def test_generate_produces_valid_python_text_gen(self):
428
+ plan = self._make_plan("text_generator")
429
+ code = self.gen.generate(plan, "text generator")
430
+ ast.parse(code)
431
+
432
+ # -- Fallback templates work without API -------------------------------
433
+
434
+ @pytest.mark.parametrize("key", list(GRADIO_TEMPLATES.keys()))
435
+ def test_fallback_template_produces_valid_python(self, key):
436
+ """Every built-in Gradio template must produce parseable Python."""
437
+ code = GRADIO_TEMPLATES[key].format(
438
+ model_id="test/model",
439
+ title="Test",
440
+ description="Test description",
441
+ )
442
+ ast.parse(code)
443
+
444
+ def test_generic_fallback_for_unknown_template(self):
445
+ plan = self._make_plan(None)
446
+ code = self.gen.generate(plan, "something unknown")
447
+ ast.parse(code)
448
+
449
+ def test_generated_code_contains_gradio_import(self):
450
+ plan = self._make_plan("chatbot")
451
+ code = self.gen.generate(plan, "chatbot")
452
+ assert "import gradio" in code
453
+
454
+ def test_generated_code_contains_launch(self):
455
+ plan = self._make_plan("chatbot")
456
+ code = self.gen.generate(plan, "chatbot")
457
+ assert "demo.launch()" in code
458
+
459
+ # -- _extract_code helper ----------------------------------------------
460
+
461
+ def test_extract_code_from_markdown_block(self):
462
+ raw = "Here is the code:\n```python\nimport gradio as gr\nprint('hi')\n```\nDone."
463
+ code = self.gen._extract_code(raw)
464
+ assert "import gradio" in code
465
+ assert "```" not in code
466
+
467
+ def test_extract_code_plain_python(self):
468
+ raw = "import gradio as gr\nprint('hi')"
469
+ code = self.gen._extract_code(raw)
470
+ assert code == raw
471
+
472
+
473
+ # ===================================================================
474
+ # 5. DockerGenerator
475
+ # ===================================================================
476
+
477
+ class TestDockerGenerator:
478
+ """Tests for DockerGenerator (template fallback, no LLM calls)."""
479
+
480
+ def setup_method(self):
481
+ self.gen = DockerGenerator()
482
+
483
+ def _make_plan(self, template_key="rest_api"):
484
+ return {
485
+ "template_key": template_key,
486
+ "title": "Test API",
487
+ "description": "A test API service",
488
+ "model_task": "text-generation",
489
+ "components": ["fastapi_app", "model_endpoint"],
490
+ "recommended_models": [
491
+ {"id": "Qwen/Qwen2.5-7B-Instruct", "desc": "Test", "size": "7B"}
492
+ ],
493
+ "extra_features": [],
494
+ }
495
+
496
+ def test_generate_produces_dockerfile(self):
497
+ files = self.gen.generate(self._make_plan(), "build an api")
498
+ assert "Dockerfile" in files
499
+
500
+ def test_generate_produces_app_py(self):
501
+ files = self.gen.generate(self._make_plan(), "build an api")
502
+ assert "app.py" in files
503
+
504
+ def test_generate_produces_requirements(self):
505
+ files = self.gen.generate(self._make_plan(), "build an api")
506
+ assert "requirements.txt" in files
507
+
508
+ def test_app_py_is_valid_python(self):
509
+ files = self.gen.generate(self._make_plan(), "build an api")
510
+ ast.parse(files["app.py"])
511
+
512
+ def test_dockerfile_has_from(self):
513
+ files = self.gen.generate(self._make_plan(), "build an api")
514
+ assert "FROM" in files["Dockerfile"]
515
+
516
+ def test_dockerfile_exposes_7860(self):
517
+ files = self.gen.generate(self._make_plan(), "build an api")
518
+ assert "7860" in files["Dockerfile"]
519
+
520
+ def test_dockerfile_has_cmd(self):
521
+ files = self.gen.generate(self._make_plan(), "build an api")
522
+ assert "CMD" in files["Dockerfile"]
523
+
524
+ def test_generic_docker_fallback(self):
525
+ plan = self._make_plan("unknown_template_key")
526
+ files = self.gen.generate(plan, "build something")
527
+ assert "app.py" in files
528
+ assert "Dockerfile" in files
529
+ ast.parse(files["app.py"])
530
+
531
+ # -- All docker templates produce valid Python -------------------------
532
+
533
+ @pytest.mark.parametrize("template_name", list(DOCKER_TEMPLATES.keys()))
534
+ def test_docker_template_app_py_is_valid_python(self, template_name):
535
+ template = DOCKER_TEMPLATES[template_name]
536
+ code = template["app.py"].format(
537
+ model_id="test/model",
538
+ title="Test",
539
+ description="Test desc",
540
+ )
541
+ ast.parse(code)
542
+
543
+ # -- _parse_files helper -----------------------------------------------
544
+
545
+ def test_parse_files_marker_format(self):
546
+ text = (
547
+ "=== FILENAME: app.py ===\nprint('hello')\n"
548
+ "=== FILENAME: requirements.txt ===\nfastapi\n"
549
+ )
550
+ files = self.gen._parse_files(text)
551
+ assert "app.py" in files
552
+ assert "requirements.txt" in files
553
+
554
+ def test_parse_files_empty_returns_empty(self):
555
+ files = self.gen._parse_files("no files here")
556
+ assert files == {}
557
+
558
+
559
+ # ===================================================================
560
+ # 6. ReadmeGenerator
561
+ # ===================================================================
562
+
563
+ class TestReadmeGenerator:
564
+ """Tests for ReadmeGenerator."""
565
+
566
+ def setup_method(self):
567
+ self.gen = ReadmeGenerator()
568
+
569
+ def _make_plan(self, app_type="chatbot", app_name="test-chatbot"):
570
+ return {
571
+ "app_type": app_type,
572
+ "title": "Test Chatbot",
573
+ "description": "A test chatbot application",
574
+ "app_name": app_name,
575
+ "recommended_models": [
576
+ {"id": "Qwen/Qwen2.5-7B-Instruct", "desc": "Test", "size": "7B"}
577
+ ],
578
+ "components": ["chat_interface", "system_prompt_config"],
579
+ }
580
+
581
+ # -- YAML frontmatter --------------------------------------------------
582
+
583
+ def test_readme_has_yaml_frontmatter(self):
584
+ readme = self.gen.generate(self._make_plan(), "gradio")
585
+ assert readme.startswith("---")
586
+ # Should have opening and closing ---
587
+ parts = readme.split("---")
588
+ assert len(parts) >= 3 # before, frontmatter, after
589
+
590
+ def test_frontmatter_contains_sdk_gradio(self):
591
+ readme = self.gen.generate(self._make_plan(), "gradio")
592
+ frontmatter = readme.split("---")[1]
593
+ assert "sdk: gradio" in frontmatter
594
+
595
+ def test_frontmatter_contains_sdk_docker(self):
596
+ readme = self.gen.generate(self._make_plan("rest_api"), "docker")
597
+ frontmatter = readme.split("---")[1]
598
+ assert "sdk: docker" in frontmatter
599
+
600
+ def test_frontmatter_contains_sdk_static(self):
601
+ readme = self.gen.generate(self._make_plan("portfolio"), "static")
602
+ frontmatter = readme.split("---")[1]
603
+ assert "sdk: static" in frontmatter
604
+
605
+ def test_frontmatter_contains_title(self):
606
+ readme = self.gen.generate(self._make_plan(), "gradio")
607
+ frontmatter = readme.split("---")[1]
608
+ assert "Test Chatbot" in frontmatter
609
+
610
+ # -- README body -------------------------------------------------------
611
+
612
+ def test_readme_contains_app_name_in_body(self):
613
+ plan = self._make_plan(app_name="my-awesome-chatbot")
614
+ plan["title"] = "My Awesome Chatbot"
615
+ readme = self.gen.generate(plan, "gradio")
616
+ # The title (which comes from the plan) should appear in the body
617
+ assert "My Awesome Chatbot" in readme
618
+
619
+ def test_readme_contains_description(self):
620
+ readme = self.gen.generate(self._make_plan(), "gradio")
621
+ assert "A test chatbot application" in readme
622
+
623
+ def test_readme_contains_features_section(self):
624
+ readme = self.gen.generate(self._make_plan(), "gradio")
625
+ assert "## Features" in readme
626
+
627
+ def test_readme_contains_model_reference(self):
628
+ readme = self.gen.generate(self._make_plan(), "gradio")
629
+ assert "Qwen/Qwen2.5-7B-Instruct" in readme
630
+
631
+ def test_readme_contains_tech_stack(self):
632
+ readme = self.gen.generate(self._make_plan(), "gradio")
633
+ assert "## Tech Stack" in readme
634
+
635
+ def test_docker_readme_mentions_fastapi(self):
636
+ readme = self.gen.generate(self._make_plan("rest_api"), "docker")
637
+ assert "FastAPI" in readme
638
+
639
+ def test_static_readme_mentions_html(self):
640
+ readme = self.gen.generate(self._make_plan("portfolio"), "static")
641
+ assert "HTML" in readme
642
+
643
+
644
+ # ===================================================================
645
+ # 7. CodeChecker
646
+ # ===================================================================
647
+
648
+ class TestCodeChecker:
649
+ """Tests for CodeChecker."""
650
+
651
+ def setup_method(self):
652
+ self.checker = CodeChecker()
653
+
654
+ # -- Python syntax validation ------------------------------------------
655
+
656
+ def test_valid_python_passes(self):
657
+ files = {"app.py": "import os\nprint('hello')\n"}
658
+ result = self.checker.check(files, "gradio")
659
+ # Should have no python-specific errors (cross-file check may warn
660
+ # about missing requirements.txt etc.)
661
+ py_check = result["file_checks"]["app.py"]
662
+ assert py_check["valid"] is True
663
+ assert len(py_check["errors"]) == 0
664
+
665
+ def test_invalid_python_syntax_caught(self):
666
+ files = {"app.py": "def foo(\n pass\n"}
667
+ result = self.checker.check(files, "gradio")
668
+ py_check = result["file_checks"]["app.py"]
669
+ assert py_check["valid"] is False
670
+ assert any("syntax error" in e.lower() for e in py_check["errors"])
671
+
672
+ def test_empty_file_flagged(self):
673
+ files = {"app.py": ""}
674
+ result = self.checker.check(files, "gradio")
675
+ py_check = result["file_checks"]["app.py"]
676
+ assert py_check["valid"] is False
677
+
678
+ def test_dangerous_pattern_warned(self):
679
+ files = {"app.py": "import os\nos.system('rm -rf /')\n"}
680
+ result = self.checker.check(files, "gradio")
681
+ py_check = result["file_checks"]["app.py"]
682
+ assert any("os.system" in w for w in py_check["warnings"])
683
+
684
+ def test_eval_warned(self):
685
+ files = {"app.py": "x = eval('1+2')\n"}
686
+ result = self.checker.check(files, "gradio")
687
+ py_check = result["file_checks"]["app.py"]
688
+ assert any("eval" in w for w in py_check["warnings"])
689
+
690
+ # -- Dockerfile validation ---------------------------------------------
691
+
692
+ def test_valid_dockerfile_passes(self):
693
+ dockerfile = (
694
+ "FROM python:3.11-slim\n"
695
+ "WORKDIR /app\n"
696
+ "COPY . .\n"
697
+ "EXPOSE 7860\n"
698
+ "CMD [\"python\", \"app.py\"]\n"
699
+ )
700
+ files = {"Dockerfile": dockerfile, "README.md": "---\ntest\n---\n"}
701
+ result = self.checker.check(files, "docker")
702
+ df_check = result["file_checks"]["Dockerfile"]
703
+ assert len(df_check["errors"]) == 0
704
+
705
+ def test_dockerfile_missing_from_is_error(self):
706
+ files = {"Dockerfile": "COPY . .\nCMD ['python']\n"}
707
+ result = self.checker.check(files, "docker")
708
+ df_check = result["file_checks"]["Dockerfile"]
709
+ assert any("FROM" in e for e in df_check["errors"])
710
+
711
+ def test_dockerfile_missing_cmd_is_error(self):
712
+ files = {"Dockerfile": "FROM python:3.11\nCOPY . .\n"}
713
+ result = self.checker.check(files, "docker")
714
+ df_check = result["file_checks"]["Dockerfile"]
715
+ assert any("CMD" in e or "ENTRYPOINT" in e for e in df_check["errors"])
716
+
717
+ def test_dockerfile_missing_expose_is_warning(self):
718
+ files = {"Dockerfile": "FROM python:3.11\nCMD ['python']\n"}
719
+ result = self.checker.check(files, "docker")
720
+ df_check = result["file_checks"]["Dockerfile"]
721
+ assert any("EXPOSE" in w for w in df_check["warnings"])
722
+
723
+ # -- Cross-file checks -------------------------------------------------
724
+
725
+ def test_gradio_missing_app_py_is_error(self):
726
+ files = {"README.md": "---\ntest\n---\n", "requirements.txt": "gradio\n"}
727
+ result = self.checker.check(files, "gradio")
728
+ assert result["valid"] is False
729
+ assert any("app.py" in e for e in result["errors"])
730
+
731
+ def test_docker_missing_dockerfile_is_error(self):
732
+ files = {"app.py": "print('hi')\n", "README.md": "---\ntest\n---\n"}
733
+ result = self.checker.check(files, "docker")
734
+ assert any("Dockerfile" in e for e in result["errors"])
735
+
736
+ def test_static_missing_index_html_is_error(self):
737
+ files = {"README.md": "---\ntest\n---\n", "style.css": "body{}\n"}
738
+ result = self.checker.check(files, "static")
739
+ assert any("index.html" in e for e in result["errors"])
740
+
741
+ def test_overall_valid_when_no_errors(self):
742
+ files = {
743
+ "app.py": "import gradio as gr\nprint('hi')\n",
744
+ "requirements.txt": "gradio>=5.0\n",
745
+ "README.md": "---\nsdk: gradio\n---\n# App\n",
746
+ }
747
+ result = self.checker.check(files, "gradio")
748
+ assert result["valid"] is True
749
+
750
+ # -- HTML validation ---------------------------------------------------
751
+
752
+ def test_valid_html_passes(self):
753
+ html = "<!DOCTYPE html><html><head></head><body></body></html>"
754
+ files = {"index.html": html}
755
+ result = self.checker.check(files, "static")
756
+ html_check = result["file_checks"]["index.html"]
757
+ assert len(html_check["errors"]) == 0
758
+
759
+ def test_html_missing_tags_warned(self):
760
+ files = {"index.html": "<div>Hello</div>"}
761
+ result = self.checker.check(files, "static")
762
+ html_check = result["file_checks"]["index.html"]
763
+ assert len(html_check["warnings"]) > 0
764
+
765
+ # -- README validation -------------------------------------------------
766
+
767
+ def test_readme_missing_frontmatter_warned(self):
768
+ files = {"README.md": "# My App\nNo frontmatter here.\n"}
769
+ result = self.checker.check(files, "gradio")
770
+ readme_check = result["file_checks"]["README.md"]
771
+ assert any("frontmatter" in w.lower() for w in readme_check["warnings"])
772
+
773
+
774
+ # ===================================================================
775
+ # 8. FastAPI App Integration Tests
776
+ # ===================================================================
777
+
778
+ @pytest.mark.skipif(
779
+ not _can_run_fastapi_tests(),
780
+ reason="Jinja2 version incompatible with Starlette TestClient in this environment",
781
+ )
782
+ class TestFastAPIApp:
783
+ """Integration tests for the FastAPI app endpoints."""
784
+
785
+ def test_home_returns_200(self, client):
786
+ resp = client.get("/")
787
+ assert resp.status_code == 200
788
+ assert "text/html" in resp.headers["content-type"]
789
+
790
+ def test_home_contains_title(self, client):
791
+ resp = client.get("/")
792
+ assert "AutoApp" in resp.text or "autoapp" in resp.text.lower() or "<html" in resp.text.lower()
793
+
794
+ def test_post_generate_returns_html(self, client):
795
+ """POST /generate with a chatbot prompt should return 200 HTML."""
796
+ resp = client.post(
797
+ "/generate",
798
+ data={
799
+ "prompt": "Build a chatbot that answers questions about science",
800
+ "sdk_preference": "auto",
801
+ "model_size": "medium",
802
+ "gpu_needed": "false",
803
+ "features": "",
804
+ },
805
+ )
806
+ assert resp.status_code == 200
807
+ assert "text/html" in resp.headers["content-type"]
808
+
809
+ def test_post_generate_contains_file_content(self, client):
810
+ """The generated result page should contain references to generated files."""
811
+ resp = client.post(
812
+ "/generate",
813
+ data={
814
+ "prompt": "Build a simple text summarizer",
815
+ "sdk_preference": "auto",
816
+ "model_size": "small",
817
+ "gpu_needed": "false",
818
+ "features": "",
819
+ },
820
+ )
821
+ assert resp.status_code == 200
822
+ # The result page should mention file names like app.py or README.md
823
+ body = resp.text.lower()
824
+ assert "app.py" in body or "readme" in body
825
+
826
+ def test_download_nonexistent_project_returns_404(self, client):
827
+ resp = client.get("/download/nonexistent")
828
+ assert resp.status_code == 404
829
+
830
+ def test_get_file_nonexistent_project_returns_404(self, client):
831
+ resp = client.get("/api/file/nonexistent/app.py")
832
+ assert resp.status_code == 404
833
+
834
+ def test_edit_nonexistent_project_returns_404(self, client):
835
+ resp = client.post(
836
+ "/edit",
837
+ data={"project_id": "nonexistent", "edit_prompt": "change colour"},
838
+ )
839
+ assert resp.status_code == 404
840
+
841
+
842
+ # ===================================================================
843
+ # 9. End-to-end generation (all SDK types, offline)
844
+ # ===================================================================
845
+
846
+ class TestEndToEnd:
847
+ """End-to-end tests exercising the full planner -> recommender -> generator
848
+ -> checker pipeline offline (no LLM calls)."""
849
+
850
+ def setup_method(self):
851
+ self.planner = AppPlanner()
852
+ self.recommender = ModelRecommender()
853
+ self.generator = RepoGenerator()
854
+ self.checker = CodeChecker()
855
+
856
+ def _run_pipeline(self, prompt, sdk_pref="auto", model_size="medium"):
857
+ plan = self.planner.analyze(prompt, sdk_pref)
858
+ models = self.recommender.recommend(plan, model_size)
859
+ plan["recommended_models"] = models
860
+ plan["extra_features"] = []
861
+ files = self.generator.generate(plan, prompt)
862
+ validation = self.checker.check(files, plan["sdk"])
863
+ return plan, files, validation
864
+
865
+ def test_chatbot_e2e(self):
866
+ plan, files, validation = self._run_pipeline("Build a chatbot")
867
+ assert plan["sdk"] == "gradio"
868
+ assert "app.py" in files
869
+ assert validation["valid"] is True
870
+
871
+ def test_image_classifier_e2e(self):
872
+ plan, files, validation = self._run_pipeline(
873
+ "Build a Gradio image classifier"
874
+ )
875
+ assert plan["sdk"] == "gradio"
876
+ assert "app.py" in files
877
+ ast.parse(files["app.py"])
878
+
879
+ def test_rest_api_e2e(self):
880
+ plan, files, validation = self._run_pipeline(
881
+ "Build a REST API with FastAPI endpoints"
882
+ )
883
+ assert plan["sdk"] == "docker"
884
+ assert "Dockerfile" in files
885
+ assert "app.py" in files
886
+ ast.parse(files["app.py"])
887
+
888
+ def test_portfolio_e2e(self):
889
+ plan, files, validation = self._run_pipeline(
890
+ "Create a portfolio website to showcase projects"
891
+ )
892
+ assert plan["sdk"] == "static"
893
+ assert "index.html" in files
894
+
895
+ def test_summarizer_small_model_e2e(self):
896
+ plan, files, validation = self._run_pipeline(
897
+ "Build a text summarization tool", model_size="small"
898
+ )
899
+ assert plan["sdk"] == "gradio"
900
+ assert "app.py" in files
901
+ assert validation["valid"] is True