7 Commits

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