Compare commits
8 Commits
tests/impr
...
feat/impro
| Author | SHA1 | Date | |
|---|---|---|---|
| 92f42ffce3 | |||
| 58408d0dbe | |||
| 2f1ac3c758 | |||
| d3b69f7459 | |||
| 50c8204fa0 | |||
| 507fe0f40e | |||
| b7b40eada1 | |||
| 9765386405 |
@@ -27,7 +27,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip install $RUNNER
|
||||
|
||||
# Set working directory for dependency installation
|
||||
WORKDIR /tmp
|
||||
WORKDIR /app
|
||||
|
||||
# Copy dependency files
|
||||
COPY pyproject.toml poetry.lock* uv.lock* Makefile ./
|
||||
@@ -43,8 +43,10 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
uv pip install --system -r pyproject.toml; \
|
||||
fi
|
||||
|
||||
COPY alfred/ ./alfred/
|
||||
COPY scripts/ ./scripts/
|
||||
COPY .env.example ./
|
||||
COPY settings.toml ./
|
||||
|
||||
# ===========================================
|
||||
# Stage 2: Testing
|
||||
@@ -62,8 +64,6 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
uv pip install --system -e .[dev]; \
|
||||
fi
|
||||
|
||||
COPY alfred/ ./alfred
|
||||
COPY scripts ./scripts
|
||||
COPY tests/ ./tests
|
||||
|
||||
# ===========================================
|
||||
@@ -120,4 +120,4 @@ EXPOSE 8000
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD python -c "import requests; requests.get('http://localhost:8000/health', timeout=5).raise_for_status()" || exit 1
|
||||
|
||||
CMD ["python", "-m", "uvicorn", "alfred.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
CMD ["python", "-m", "uvicorn", "alfred.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
@@ -41,10 +41,10 @@ def _get_secret_factory(rule: str):
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""
|
||||
Application settings.
|
||||
Application settings management.
|
||||
|
||||
Settings are loaded from .env and validated using the schema
|
||||
defined in pyproject.toml.
|
||||
Initializes configuration with internal defaults from the settings module,
|
||||
then overrides them with environment variables loaded from a .env file.
|
||||
"""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
|
||||
@@ -185,39 +185,26 @@ def load_schema(base_dir: Path | None = None) -> SettingsSchema:
|
||||
if base_dir is None:
|
||||
base_dir = BASE_DIR
|
||||
|
||||
# Try settings.toml first (cleaner, dedicated file)
|
||||
# Load from settings.toml (required)
|
||||
settings_toml_path = base_dir / "settings.toml"
|
||||
if settings_toml_path.exists():
|
||||
with open(settings_toml_path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
|
||||
try:
|
||||
schema_dict = data["tool"]["alfred"]["settings_schema"]
|
||||
return SettingsSchema(schema_dict)
|
||||
except KeyError as e:
|
||||
raise KeyError(
|
||||
"Missing [tool.alfred.settings_schema] section in settings.toml"
|
||||
) from e
|
||||
|
||||
# Fallback to pyproject.toml
|
||||
toml_path = base_dir / "pyproject.toml"
|
||||
if not toml_path.exists():
|
||||
if not settings_toml_path.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Neither settings.toml nor pyproject.toml found in {base_dir}"
|
||||
f"settings.toml not found at {settings_toml_path}. "
|
||||
f"This file is required and must be present in the application root."
|
||||
)
|
||||
|
||||
with open(toml_path, "rb") as f:
|
||||
with open(settings_toml_path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
|
||||
try:
|
||||
schema_dict = data["tool"]["alfred"]["settings_schema"]
|
||||
return SettingsSchema(schema_dict)
|
||||
except KeyError as e:
|
||||
raise KeyError(
|
||||
"Missing [tool.alfred.settings_schema] section in pyproject.toml"
|
||||
f"Missing [tool.alfred.settings_schema] section in settings.toml at {settings_toml_path}"
|
||||
) from e
|
||||
|
||||
return SettingsSchema(schema_dict)
|
||||
|
||||
|
||||
def validate_value(definition: SettingDefinition, value: Any) -> bool:
|
||||
"""
|
||||
|
||||
@@ -17,6 +17,7 @@ services:
|
||||
# --- MAIN APPLICATION ---
|
||||
alfred:
|
||||
container_name: alfred-core
|
||||
image: alfred_media_organizer:latest
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
@@ -34,7 +35,7 @@ services:
|
||||
- ./data:/data
|
||||
- ./logs:/logs
|
||||
# TODO: Hot reload (comment out in production)
|
||||
- ./alfred:/home/appuser/alfred
|
||||
#- ./alfred:/home/appuser/alfred
|
||||
command: >
|
||||
sh -c "python -u -m uvicorn alfred.app:app --host 0.0.0.0 --port 8000 2>&1 | tee -a /logs/alfred.log"
|
||||
networks:
|
||||
@@ -169,7 +170,7 @@ services:
|
||||
- ./data/vectordb:/var/lib/postgresql/data
|
||||
profiles: ["rag", "full"]
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-alfred} -d $${POSTGRES_DB_NAME:-alfred}" ]
|
||||
test: [ "CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB_NAME}" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
12
poetry.lock
generated
12
poetry.lock
generated
@@ -74,13 +74,13 @@ wcmatch = ">=8.5.1"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.11.12"
|
||||
version = "2026.1.4"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"},
|
||||
{file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"},
|
||||
{file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"},
|
||||
{file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -394,13 +394,13 @@ standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[stand
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.20.1"
|
||||
version = "3.20.2"
|
||||
description = "A platform independent file lock."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a"},
|
||||
{file = "filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c"},
|
||||
{file = "filelock-3.20.2-py3-none-any.whl", hash = "sha256:fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8"},
|
||||
{file = "filelock-3.20.2.tar.gz", hash = "sha256:a2241ff4ddde2a7cebddf78e39832509cb045d18ec1a09d7248d6bfc6bfbbe64"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -12,8 +12,8 @@ from alfred.settings_bootstrap import (
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_toml_content():
|
||||
"""Test TOML content with schema."""
|
||||
def test_pyproject_content():
|
||||
"""Test pyproject.toml content with poetry metadata."""
|
||||
return """
|
||||
[tool.poetry]
|
||||
name = "test"
|
||||
@@ -25,7 +25,13 @@ python = "==3.14.2"
|
||||
[tool.alfred.settings]
|
||||
runner = "poetry"
|
||||
image_name = "test_image"
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_settings_content():
|
||||
"""Test settings.toml content with schema."""
|
||||
return """
|
||||
[tool.alfred.settings_schema.TEST_FROM_TOML]
|
||||
type = "string"
|
||||
source = "toml"
|
||||
@@ -72,11 +78,15 @@ def config_source(tmp_path):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def create_test_env(tmp_path, test_toml_content):
|
||||
def create_test_env(tmp_path, test_pyproject_content, test_settings_content):
|
||||
"""Create a complete test environment."""
|
||||
# Create pyproject.toml
|
||||
toml_path = tmp_path / "pyproject.toml"
|
||||
toml_path.write_text(test_toml_content)
|
||||
pyproject_path = tmp_path / "pyproject.toml"
|
||||
pyproject_path.write_text(test_pyproject_content)
|
||||
|
||||
# Create settings.toml
|
||||
settings_path = tmp_path / "settings.toml"
|
||||
settings_path.write_text(test_settings_content)
|
||||
|
||||
# Create .env.example
|
||||
env_example = tmp_path / ".env.example"
|
||||
@@ -330,11 +340,17 @@ TEST_SECRET=my_secret_123
|
||||
env_content = create_test_env.env_path.read_text()
|
||||
assert "TEST_SECRET=my_secret_123" in env_content
|
||||
|
||||
def test_validation_error(self, tmp_path, test_toml_content):
|
||||
def test_validation_error(
|
||||
self, tmp_path, test_pyproject_content, test_settings_content
|
||||
):
|
||||
"""Test validation error is raised."""
|
||||
# Add a setting with validation
|
||||
toml_with_validation = (
|
||||
test_toml_content
|
||||
# Create pyproject.toml
|
||||
pyproject_path = tmp_path / "pyproject.toml"
|
||||
pyproject_path.write_text(test_pyproject_content)
|
||||
|
||||
# Add a setting with validation to settings.toml
|
||||
settings_with_validation = (
|
||||
test_settings_content
|
||||
+ """
|
||||
[tool.alfred.settings_schema.TEST_VALIDATED]
|
||||
type = "integer"
|
||||
@@ -345,8 +361,8 @@ description = "Validated setting"
|
||||
category = "test"
|
||||
"""
|
||||
)
|
||||
toml_path = tmp_path / "pyproject.toml"
|
||||
toml_path.write_text(toml_with_validation)
|
||||
settings_path = tmp_path / "settings.toml"
|
||||
settings_path.write_text(settings_with_validation)
|
||||
|
||||
env_example = tmp_path / ".env.example"
|
||||
env_example.write_text("TEST_VALIDATED=\n")
|
||||
@@ -364,9 +380,10 @@ category = "test"
|
||||
|
||||
def test_write_env_make_only_exports(self, create_test_env):
|
||||
"""Test that .env.make only contains export_to_env_make settings."""
|
||||
# Add a setting with export_to_env_make
|
||||
toml_content = create_test_env.toml_path.read_text()
|
||||
toml_content += """
|
||||
# Add a setting with export_to_env_make to settings.toml
|
||||
settings_path = create_test_env.base_dir / "settings.toml"
|
||||
settings_content = settings_path.read_text()
|
||||
settings_content += """
|
||||
[tool.alfred.settings_schema.EXPORTED_VAR]
|
||||
type = "string"
|
||||
source = "env"
|
||||
@@ -374,7 +391,7 @@ default = "exported"
|
||||
export_to_env_make = true
|
||||
category = "build"
|
||||
"""
|
||||
create_test_env.toml_path.write_text(toml_content)
|
||||
settings_path.write_text(settings_content)
|
||||
|
||||
# Recreate schema
|
||||
from alfred.settings_schema import load_schema
|
||||
|
||||
@@ -8,14 +8,19 @@ from alfred.settings_bootstrap import ConfigSource, SettingsBootstrap
|
||||
@pytest.fixture
|
||||
def test_toml_with_all_types(tmp_path):
|
||||
"""Create test TOML with all setting types."""
|
||||
toml_content = """
|
||||
# Create pyproject.toml for poetry metadata
|
||||
pyproject_content = """
|
||||
[tool.poetry]
|
||||
name = "test"
|
||||
version = "1.0.0"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.14"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||
|
||||
# Create settings.toml with schema
|
||||
settings_content = """
|
||||
[tool.alfred.settings_schema.STRING_VAR]
|
||||
type = "string"
|
||||
source = "env"
|
||||
@@ -59,14 +64,9 @@ compute_template = "{STRING_VAR}_{INT_VAR}"
|
||||
description = "Computed variable"
|
||||
category = "test"
|
||||
"""
|
||||
toml_path = tmp_path / "pyproject.toml"
|
||||
toml_path.write_text(toml_content)
|
||||
return tmp_path
|
||||
(tmp_path / "settings.toml").write_text(settings_content)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_env_example(tmp_path):
|
||||
"""Create .env.example template."""
|
||||
# Create .env.example
|
||||
env_example_content = """# Test configuration
|
||||
STRING_VAR=
|
||||
INT_VAR=
|
||||
@@ -81,18 +81,18 @@ SECRET_VAR=
|
||||
# Computed values
|
||||
COMPUTED_VAR=
|
||||
|
||||
# Custom section
|
||||
# Custom variable (not in schema)
|
||||
CUSTOM_VAR=custom_value
|
||||
"""
|
||||
env_example_path = tmp_path / ".env.example"
|
||||
env_example_path.write_text(env_example_content)
|
||||
return env_example_path
|
||||
(tmp_path / ".env.example").write_text(env_example_content)
|
||||
|
||||
return tmp_path
|
||||
|
||||
|
||||
class TestTemplatePreservation:
|
||||
"""Test that .env.example template structure is preserved."""
|
||||
|
||||
def test_preserves_comments(self, test_toml_with_all_types, test_env_example):
|
||||
def test_preserves_comments(self, test_toml_with_all_types):
|
||||
"""Test that comments from .env.example are preserved."""
|
||||
from alfred.settings_schema import load_schema
|
||||
|
||||
@@ -110,7 +110,7 @@ class TestTemplatePreservation:
|
||||
assert "# Security" in env_content
|
||||
assert "# Computed values" in env_content
|
||||
|
||||
def test_preserves_empty_lines(self, test_toml_with_all_types, test_env_example):
|
||||
def test_preserves_empty_lines(self, test_toml_with_all_types):
|
||||
"""Test that empty lines from .env.example are preserved."""
|
||||
from alfred.settings_schema import load_schema
|
||||
|
||||
@@ -126,7 +126,7 @@ class TestTemplatePreservation:
|
||||
# Check there are empty lines (structure preserved)
|
||||
assert "" in lines
|
||||
|
||||
def test_preserves_variable_order(self, test_toml_with_all_types, test_env_example):
|
||||
def test_preserves_variable_order(self, test_toml_with_all_types):
|
||||
"""Test that variable order from .env.example is preserved."""
|
||||
from alfred.settings_schema import load_schema
|
||||
|
||||
@@ -150,9 +150,7 @@ class TestTemplatePreservation:
|
||||
class TestSecretPreservation:
|
||||
"""Test that secrets are never overwritten."""
|
||||
|
||||
def test_preserves_existing_secrets(
|
||||
self, test_toml_with_all_types, test_env_example
|
||||
):
|
||||
def test_preserves_existing_secrets(self, test_toml_with_all_types):
|
||||
"""Test that existing secrets are preserved across multiple bootstraps."""
|
||||
from alfred.settings_schema import load_schema
|
||||
|
||||
@@ -186,11 +184,14 @@ class TestSecretPreservation:
|
||||
|
||||
def test_multiple_secrets_preserved(self, tmp_path):
|
||||
"""Test that multiple secrets are all preserved."""
|
||||
toml_content = """
|
||||
pyproject_content = """
|
||||
[tool.poetry]
|
||||
name = "test"
|
||||
version = "1.0.0"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||
|
||||
settings_content = """
|
||||
[tool.alfred.settings_schema.SECRET_1]
|
||||
type = "secret"
|
||||
source = "generated"
|
||||
@@ -209,7 +210,7 @@ source = "generated"
|
||||
secret_rule = "8:hex"
|
||||
category = "security"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
||||
(tmp_path / "settings.toml").write_text(settings_content)
|
||||
(tmp_path / ".env.example").write_text("SECRET_1=\nSECRET_2=\nSECRET_3=\n")
|
||||
|
||||
from alfred.settings_schema import load_schema
|
||||
@@ -236,9 +237,7 @@ category = "security"
|
||||
class TestCustomVariables:
|
||||
"""Test that custom variables (not in schema) are preserved."""
|
||||
|
||||
def test_preserves_custom_variables_from_env(
|
||||
self, test_toml_with_all_types, test_env_example
|
||||
):
|
||||
def test_preserves_custom_variables_from_env(self, test_toml_with_all_types):
|
||||
"""Test that custom variables added to .env are preserved."""
|
||||
from alfred.settings_schema import load_schema
|
||||
|
||||
@@ -264,9 +263,7 @@ class TestCustomVariables:
|
||||
assert "MY_CUSTOM_VAR=custom_value" in env_content
|
||||
assert "ANOTHER_CUSTOM=another_value" in env_content
|
||||
|
||||
def test_custom_variables_in_dedicated_section(
|
||||
self, test_toml_with_all_types, test_env_example
|
||||
):
|
||||
def test_custom_variables_in_dedicated_section(self, test_toml_with_all_types):
|
||||
"""Test that custom variables are placed in a dedicated section."""
|
||||
from alfred.settings_schema import load_schema
|
||||
|
||||
@@ -285,9 +282,7 @@ class TestCustomVariables:
|
||||
assert "# --- CUSTOM VARIABLES ---" in env_content
|
||||
assert "MY_CUSTOM_VAR=test" in env_content
|
||||
|
||||
def test_preserves_custom_from_env_example(
|
||||
self, test_toml_with_all_types, test_env_example
|
||||
):
|
||||
def test_preserves_custom_from_env_example(self, test_toml_with_all_types):
|
||||
"""Test that custom variables in .env.example are preserved."""
|
||||
from alfred.settings_schema import load_schema
|
||||
|
||||
@@ -306,9 +301,7 @@ class TestCustomVariables:
|
||||
class TestBooleanHandling:
|
||||
"""Test that booleans are handled correctly."""
|
||||
|
||||
def test_booleans_written_as_lowercase(
|
||||
self, test_toml_with_all_types, test_env_example
|
||||
):
|
||||
def test_booleans_written_as_lowercase(self, test_toml_with_all_types):
|
||||
"""Test that Python booleans are written as lowercase strings."""
|
||||
from alfred.settings_schema import load_schema
|
||||
|
||||
@@ -327,18 +320,21 @@ class TestBooleanHandling:
|
||||
|
||||
def test_false_boolean_written_as_lowercase(self, tmp_path):
|
||||
"""Test that False is written as 'false'."""
|
||||
toml_content = """
|
||||
pyproject_content = """
|
||||
[tool.poetry]
|
||||
name = "test"
|
||||
version = "1.0.0"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||
|
||||
settings_content = """
|
||||
[tool.alfred.settings_schema.BOOL_FALSE]
|
||||
type = "boolean"
|
||||
source = "env"
|
||||
default = false
|
||||
category = "test"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
||||
(tmp_path / "settings.toml").write_text(settings_content)
|
||||
(tmp_path / ".env.example").write_text("BOOL_FALSE=\n")
|
||||
|
||||
from alfred.settings_schema import load_schema
|
||||
@@ -356,18 +352,21 @@ category = "test"
|
||||
|
||||
def test_boolean_parsing_from_env(self, tmp_path):
|
||||
"""Test that various boolean formats are parsed correctly."""
|
||||
toml_content = """
|
||||
pyproject_content = """
|
||||
[tool.poetry]
|
||||
name = "test"
|
||||
version = "1.0.0"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||
|
||||
settings_content = """
|
||||
[tool.alfred.settings_schema.BOOL_VAR]
|
||||
type = "boolean"
|
||||
source = "env"
|
||||
default = false
|
||||
category = "test"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
||||
(tmp_path / "settings.toml").write_text(settings_content)
|
||||
(tmp_path / ".env.example").write_text("BOOL_VAR=\n")
|
||||
|
||||
from alfred.settings_schema import load_schema
|
||||
@@ -402,9 +401,7 @@ category = "test"
|
||||
class TestComputedVariables:
|
||||
"""Test that computed variables are calculated correctly."""
|
||||
|
||||
def test_computed_variables_written_to_env(
|
||||
self, test_toml_with_all_types, test_env_example
|
||||
):
|
||||
def test_computed_variables_written_to_env(self, test_toml_with_all_types):
|
||||
"""Test that computed variables are written with their computed values."""
|
||||
from alfred.settings_schema import load_schema
|
||||
|
||||
@@ -421,11 +418,14 @@ class TestComputedVariables:
|
||||
|
||||
def test_computed_uri_example(self, tmp_path):
|
||||
"""Test computed URI (like MONGO_URI) is written correctly."""
|
||||
toml_content = """
|
||||
pyproject_content = """
|
||||
[tool.poetry]
|
||||
name = "test"
|
||||
version = "1.0.0"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||
|
||||
settings_content = """
|
||||
[tool.alfred.settings_schema.DB_HOST]
|
||||
type = "string"
|
||||
source = "env"
|
||||
@@ -457,7 +457,7 @@ compute_from = ["DB_USER", "DB_PASSWORD", "DB_HOST", "DB_PORT"]
|
||||
compute_template = "postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/db"
|
||||
category = "database"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
||||
(tmp_path / "settings.toml").write_text(settings_content)
|
||||
(tmp_path / ".env.example").write_text(
|
||||
"DB_HOST=\nDB_PORT=\nDB_USER=\nDB_PASSWORD=\nDB_URI=\n"
|
||||
)
|
||||
@@ -491,18 +491,21 @@ class TestEdgeCases:
|
||||
|
||||
def test_missing_env_example(self, tmp_path):
|
||||
"""Test that missing .env.example raises error."""
|
||||
toml_content = """
|
||||
pyproject_content = """
|
||||
[tool.poetry]
|
||||
name = "test"
|
||||
version = "1.0.0"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||
|
||||
settings_content = """
|
||||
[tool.alfred.settings_schema.TEST_VAR]
|
||||
type = "string"
|
||||
source = "env"
|
||||
default = "test"
|
||||
category = "test"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
||||
(tmp_path / "settings.toml").write_text(settings_content)
|
||||
|
||||
from alfred.settings_schema import load_schema
|
||||
|
||||
@@ -516,18 +519,21 @@ category = "test"
|
||||
|
||||
def test_empty_env_example(self, tmp_path):
|
||||
"""Test that empty .env.example works."""
|
||||
toml_content = """
|
||||
pyproject_content = """
|
||||
[tool.poetry]
|
||||
name = "test"
|
||||
version = "1.0.0"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||
|
||||
settings_content = """
|
||||
[tool.alfred.settings_schema.TEST_VAR]
|
||||
type = "string"
|
||||
source = "env"
|
||||
default = "test"
|
||||
category = "test"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
||||
(tmp_path / "settings.toml").write_text(settings_content)
|
||||
(tmp_path / ".env.example").write_text("")
|
||||
|
||||
from alfred.settings_schema import load_schema
|
||||
@@ -543,18 +549,21 @@ category = "test"
|
||||
|
||||
def test_variable_with_equals_in_value(self, tmp_path):
|
||||
"""Test that variables with '=' in their value are handled correctly."""
|
||||
toml_content = """
|
||||
pyproject_content = """
|
||||
[tool.poetry]
|
||||
name = "test"
|
||||
version = "1.0.0"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||
|
||||
settings_content = """
|
||||
[tool.alfred.settings_schema.URL_VAR]
|
||||
type = "string"
|
||||
source = "env"
|
||||
default = "http://example.com?key=value"
|
||||
category = "test"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
||||
(tmp_path / "settings.toml").write_text(settings_content)
|
||||
(tmp_path / ".env.example").write_text("URL_VAR=\n")
|
||||
|
||||
from alfred.settings_schema import load_schema
|
||||
@@ -571,11 +580,14 @@ category = "test"
|
||||
|
||||
def test_preserves_existing_values_on_update(self, tmp_path):
|
||||
"""Test that existing values are preserved when updating."""
|
||||
toml_content = """
|
||||
pyproject_content = """
|
||||
[tool.poetry]
|
||||
name = "test"
|
||||
version = "1.0.0"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||
|
||||
settings_content = """
|
||||
[tool.alfred.settings_schema.VAR1]
|
||||
type = "string"
|
||||
source = "env"
|
||||
@@ -588,7 +600,7 @@ source = "env"
|
||||
default = "default2"
|
||||
category = "test"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
||||
(tmp_path / "settings.toml").write_text(settings_content)
|
||||
(tmp_path / ".env.example").write_text("VAR1=\nVAR2=\n")
|
||||
|
||||
from alfred.settings_schema import load_schema
|
||||
@@ -619,14 +631,17 @@ class TestIntegration:
|
||||
|
||||
def test_full_workflow_like_alfred(self, tmp_path):
|
||||
"""Test a full workflow similar to Alfred's actual usage."""
|
||||
toml_content = """
|
||||
pyproject_content = """
|
||||
[tool.poetry]
|
||||
name = "alfred"
|
||||
version = "0.1.7"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.14"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||
|
||||
settings_content = """
|
||||
[tool.alfred.settings_schema.ALFRED_VERSION]
|
||||
type = "string"
|
||||
source = "toml"
|
||||
@@ -677,7 +692,7 @@ source = "env"
|
||||
default = false
|
||||
category = "app"
|
||||
"""
|
||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
||||
(tmp_path / "settings.toml").write_text(settings_content)
|
||||
|
||||
env_example_content = """# Application settings
|
||||
HOST=0.0.0.0
|
||||
@@ -715,7 +730,12 @@ ALFRED_VERSION=
|
||||
assert "DEBUG_MODE=false" in env_content_1 # lowercase!
|
||||
assert "ALFRED_VERSION=0.1.7" in env_content_1
|
||||
assert "JWT_SECRET=" in env_content_1
|
||||
assert len([l for l in env_content_1.split("\n") if "JWT_SECRET=" in l][0]) > 20
|
||||
assert (
|
||||
len(
|
||||
[line for line in env_content_1.split("\n") if "JWT_SECRET=" in line][0]
|
||||
)
|
||||
> 20
|
||||
)
|
||||
assert "MONGO_URI=mongodb://user:" in env_content_1
|
||||
|
||||
# Second bootstrap - should preserve everything
|
||||
|
||||
@@ -60,7 +60,7 @@ def create_schema_file(tmp_path):
|
||||
"""Factory to create pyproject.toml with schema."""
|
||||
|
||||
def _create(content: str):
|
||||
toml_path = tmp_path / "pyproject.toml"
|
||||
toml_path = tmp_path / "settings.toml"
|
||||
full_content = f"""
|
||||
[tool.poetry]
|
||||
name = "test"
|
||||
@@ -188,7 +188,7 @@ default = true
|
||||
|
||||
def test_missing_schema_section(self, tmp_path):
|
||||
"""Test error when schema section is missing."""
|
||||
toml_path = tmp_path / "pyproject.toml"
|
||||
toml_path = tmp_path / "settings.toml"
|
||||
toml_path.write_text("""
|
||||
[tool.poetry]
|
||||
name = "test"
|
||||
|
||||
Reference in New Issue
Block a user