5 Commits

6 changed files with 878 additions and 48 deletions

View File

@@ -40,7 +40,7 @@ MONGO_HOST=mongodb
MONGO_PORT=27017 MONGO_PORT=27017
MONGO_USER=alfred MONGO_USER=alfred
MONGO_PASSWORD= MONGO_PASSWORD=
MONGO_DB_NAME=alfred MONGO_DB_NAME=LibreChat
# PostgreSQL (Vector Database / RAG) # PostgreSQL (Vector Database / RAG)
POSTGRES_URI= POSTGRES_URI=

View File

@@ -108,6 +108,7 @@ COPY --chown=appuser:appuser alfred/ ./alfred
COPY --chown=appuser:appuser scripts/ ./scripts COPY --chown=appuser:appuser scripts/ ./scripts
COPY --chown=appuser:appuser .env.example ./ COPY --chown=appuser:appuser .env.example ./
COPY --chown=appuser:appuser pyproject.toml ./ COPY --chown=appuser:appuser pyproject.toml ./
COPY --chown=appuser:appuser settings.toml ./
# Create volumes for persistent data # Create volumes for persistent data
VOLUME ["/data", "/logs"] VOLUME ["/data", "/logs"]

View File

@@ -241,16 +241,18 @@ class SettingsBootstrap:
else: else:
raise ValueError(f"{definition.name} is required but not found in .env") raise ValueError(f"{definition.name} is required but not found in .env")
# Convert type # Convert type (only if value is a string from .env)
match definition.type: match definition.type:
case SettingType.INTEGER: case SettingType.INTEGER:
return int(value) return int(value) if not isinstance(value, int) else value
case SettingType.FLOAT: case SettingType.FLOAT:
return float(value) return float(value) if not isinstance(value, float) else value
case SettingType.BOOLEAN: case SettingType.BOOLEAN:
return value.lower() in ("true", "1", "yes") if isinstance(value, bool):
return value
return str(value).lower() in ("true", "1", "yes")
case _: case _:
return value return str(value) if not isinstance(value, str) else value
def _resolve_generated(self, definition: SettingDefinition) -> str: def _resolve_generated(self, definition: SettingDefinition) -> str:
"""Resolve generated secret.""" """Resolve generated secret."""
@@ -307,49 +309,83 @@ class SettingsBootstrap:
) )
def _write_env(self) -> None: def _write_env(self) -> None:
"""Write .env file.""" """
Write .env file using .env.example as template.
This preserves the structure, comments, and formatting of .env.example
while updating only the values of variables defined in the schema.
Custom variables from existing .env are appended at the end.
"""
print("📝 Writing .env...") print("📝 Writing .env...")
lines = [] # Check if .env.example exists
lines.append("# Auto-generated by bootstrap - DO NOT EDIT MANUALLY\n") if not self.source.env_example_path.exists():
lines.append("# Edit pyproject.toml [tool.alfred.settings_schema] instead\n") raise FileNotFoundError(
lines.append("\n") f".env.example not found: {self.source.env_example_path}"
)
# Group by category # Read .env.example as template
categories = {} with open(self.source.env_example_path, encoding="utf-8") as f:
for definition in self.schema: template_lines = f.readlines()
if definition.category not in categories:
categories[definition.category] = []
categories[definition.category].append(definition)
# Write by category # Track which keys we've processed from .env.example
for category, definitions in sorted(categories.items()): processed_keys = set()
lines.append(f"# --- {category.upper()} ---\n")
for definition in definitions:
# Skip computed settings (they're not in .env)
if definition.source == SettingSource.COMPUTED:
continue
value = self.resolved_settings.get(definition.name, "") # Process template line by line
if definition.description: output_lines = []
lines.append(f"# {definition.description}\n") for line in template_lines:
lines.append(f"{definition.name}={value}\n") stripped = line.strip()
lines.append("\n")
# Write computed settings at the end # Keep comments and empty lines as-is
computed_defs = [d for d in self.schema if d.source == SettingSource.COMPUTED] if not stripped or stripped.startswith("#"):
if computed_defs: output_lines.append(line)
lines.append("# --- COMPUTED (auto-generated) ---\n") continue
for definition in computed_defs:
value = self.resolved_settings.get(definition.name, "")
if definition.description:
lines.append(f"# {definition.description}\n")
lines.append(f"{definition.name}={value}\n")
# Check if line contains a variable assignment
if "=" in line:
key, _ = line.split("=", 1)
key = key.strip()
processed_keys.add(key)
# Check if this variable is in our schema
definition = self.schema.get(key)
if definition:
# Update with resolved value (including computed settings)
value = self.resolved_settings.get(key, "")
# Convert Python booleans to lowercase for .env compatibility
if isinstance(value, bool):
value = "true" if value else "false"
output_lines.append(f"{key}={value}\n")
# Variable not in schema
# If it exists in current .env, use that value, otherwise keep template
elif key in self.existing_env:
output_lines.append(f"{key}={self.existing_env[key]}\n")
else:
output_lines.append(line)
else:
# Keep any other lines as-is
output_lines.append(line)
# Append custom variables from existing .env that aren't in .env.example
custom_vars = {
k: v for k, v in self.existing_env.items() if k not in processed_keys
}
if custom_vars:
output_lines.append("\n# --- CUSTOM VARIABLES ---\n")
output_lines.append("# Variables added manually (not in .env.example)\n")
for key, value in sorted(custom_vars.items()):
output_lines.append(f"{key}={value}\n")
# Write updated .env
with open(self.source.env_path, "w", encoding="utf-8") as f: with open(self.source.env_path, "w", encoding="utf-8") as f:
f.writelines(lines) f.writelines(output_lines)
print(f"{self.source.env_path.name} written") print(f"{self.source.env_path.name} written (preserving template structure)")
if custom_vars:
print(f" Preserved {len(custom_vars)} custom variable(s)")
def _write_env_make(self) -> None: def _write_env_make(self) -> None:
"""Write .env.make for Makefile.""" """Write .env.make for Makefile."""

View File

@@ -34,7 +34,9 @@ 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: >
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:
- alfred-net - alfred-net
@@ -84,12 +86,11 @@ services:
ports: ports:
- "${MONGO_PORT}:${MONGO_PORT}" - "${MONGO_PORT}:${MONGO_PORT}"
volumes: volumes:
- ./data/mongo:/data/db - ./data/mongodb:/data/db
command: mongod --quiet --setParameter logComponentVerbosity='{"network":{"verbosity":0}}' - ./mongod.conf:/etc/mongod.conf:ro
command: ["mongod", "--config", "/etc/mongod.conf"]
healthcheck: healthcheck:
test: | test: mongosh --quiet -u "${MONGO_USER}" -p "${MONGO_PASSWORD}" --authenticationDatabase admin --eval "db.adminCommand('ping')"
mongosh --quiet --eval "db.adminCommand('ping')" || \
mongosh --quiet -u "${MONGO_USER}" -p "${MONGO_PASSWORD}" --authenticationDatabase admin --eval "db.adminCommand('ping')"
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -168,12 +169,14 @@ 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:-alfred} -d ${POSTGRES_DB_NAME:-alfred}" ] 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
networks: networks:
- alfred-net alfred-net:
aliases:
- db
# --- QBITTORENT (Optional) --- # --- QBITTORENT (Optional) ---
qbittorrent: qbittorrent:

49
mongod.conf Normal file
View File

@@ -0,0 +1,49 @@
# MongoDB Configuration File
# Network settings
net:
port: 27017
bindIp: 0.0.0.0
# Storage settings
storage:
dbPath: /data/db
# Security settings
security:
authorization: enabled
# System log settings
systemLog:
destination: file
path: /dev/stdout
logAppend: false
verbosity: 0
quiet: true
component:
accessControl:
verbosity: -1
command:
verbosity: 0
control:
verbosity: 0
ftdc:
verbosity: 0
geo:
verbosity: 0
index:
verbosity: 0
network:
verbosity: 0
query:
verbosity: 0
replication:
verbosity: 0
sharding:
verbosity: 0
storage:
verbosity: 0
write:
verbosity: 0
transaction:
verbosity: 0

View File

@@ -0,0 +1,741 @@
"""Advanced tests for settings bootstrap - template preservation and edge cases."""
import pytest
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 = """
[tool.poetry]
name = "test"
version = "1.0.0"
[tool.poetry.dependencies]
python = "^3.14"
[tool.alfred.settings_schema.STRING_VAR]
type = "string"
source = "env"
default = "default_string"
description = "String variable"
category = "test"
[tool.alfred.settings_schema.INT_VAR]
type = "integer"
source = "env"
default = 42
description = "Integer variable"
category = "test"
[tool.alfred.settings_schema.FLOAT_VAR]
type = "float"
source = "env"
default = 3.14
description = "Float variable"
category = "test"
[tool.alfred.settings_schema.BOOL_VAR]
type = "boolean"
source = "env"
default = true
description = "Boolean variable"
category = "test"
[tool.alfred.settings_schema.SECRET_VAR]
type = "secret"
source = "generated"
secret_rule = "16:hex"
description = "Secret variable"
category = "security"
[tool.alfred.settings_schema.COMPUTED_VAR]
type = "computed"
source = "computed"
compute_from = ["STRING_VAR", "INT_VAR"]
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
@pytest.fixture
def test_env_example(tmp_path):
"""Create .env.example template."""
env_example_content = """# Test configuration
STRING_VAR=
INT_VAR=
FLOAT_VAR=
# Boolean settings
BOOL_VAR=
# Security
SECRET_VAR=
# Computed values
COMPUTED_VAR=
# Custom section
CUSTOM_VAR=custom_value
"""
env_example_path = tmp_path / ".env.example"
env_example_path.write_text(env_example_content)
return env_example_path
class TestTemplatePreservation:
"""Test that .env.example template structure is preserved."""
def test_preserves_comments(self, test_toml_with_all_types, test_env_example):
"""Test that comments from .env.example are preserved."""
from alfred.settings_schema import load_schema
schema = load_schema(test_toml_with_all_types)
source = ConfigSource.from_base_dir(test_toml_with_all_types)
bootstrapper = SettingsBootstrap(source, schema)
bootstrapper.bootstrap()
env_content = source.env_path.read_text()
# Check comments are preserved
assert "# Test configuration" in env_content
assert "# Boolean settings" in env_content
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):
"""Test that empty lines from .env.example are preserved."""
from alfred.settings_schema import load_schema
schema = load_schema(test_toml_with_all_types)
source = ConfigSource.from_base_dir(test_toml_with_all_types)
bootstrapper = SettingsBootstrap(source, schema)
bootstrapper.bootstrap()
env_content = source.env_path.read_text()
lines = env_content.split("\n")
# Check there are empty lines (structure preserved)
assert "" in lines
def test_preserves_variable_order(self, test_toml_with_all_types, test_env_example):
"""Test that variable order from .env.example is preserved."""
from alfred.settings_schema import load_schema
schema = load_schema(test_toml_with_all_types)
source = ConfigSource.from_base_dir(test_toml_with_all_types)
bootstrapper = SettingsBootstrap(source, schema)
bootstrapper.bootstrap()
env_content = source.env_path.read_text()
# Check order is preserved
string_pos = env_content.find("STRING_VAR=")
int_pos = env_content.find("INT_VAR=")
float_pos = env_content.find("FLOAT_VAR=")
bool_pos = env_content.find("BOOL_VAR=")
assert string_pos < int_pos < float_pos < bool_pos
class TestSecretPreservation:
"""Test that secrets are never overwritten."""
def test_preserves_existing_secrets(
self, test_toml_with_all_types, test_env_example
):
"""Test that existing secrets are preserved across multiple bootstraps."""
from alfred.settings_schema import load_schema
schema = load_schema(test_toml_with_all_types)
source = ConfigSource.from_base_dir(test_toml_with_all_types)
# First bootstrap - generates secret
bootstrapper1 = SettingsBootstrap(source, schema)
bootstrapper1.bootstrap()
env_content_1 = source.env_path.read_text()
secret_1 = [
line.split("=")[1]
for line in env_content_1.split("\n")
if line.startswith("SECRET_VAR=")
][0]
# Second bootstrap - should preserve secret
bootstrapper2 = SettingsBootstrap(source, schema)
bootstrapper2.bootstrap()
env_content_2 = source.env_path.read_text()
secret_2 = [
line.split("=")[1]
for line in env_content_2.split("\n")
if line.startswith("SECRET_VAR=")
][0]
assert secret_1 == secret_2
assert len(secret_1) == 32 # 16 hex bytes
def test_multiple_secrets_preserved(self, tmp_path):
"""Test that multiple secrets are all preserved."""
toml_content = """
[tool.poetry]
name = "test"
version = "1.0.0"
[tool.alfred.settings_schema.SECRET_1]
type = "secret"
source = "generated"
secret_rule = "16:hex"
category = "security"
[tool.alfred.settings_schema.SECRET_2]
type = "secret"
source = "generated"
secret_rule = "32:b64"
category = "security"
[tool.alfred.settings_schema.SECRET_3]
type = "secret"
source = "generated"
secret_rule = "8:hex"
category = "security"
"""
(tmp_path / "pyproject.toml").write_text(toml_content)
(tmp_path / ".env.example").write_text("SECRET_1=\nSECRET_2=\nSECRET_3=\n")
from alfred.settings_schema import load_schema
schema = load_schema(tmp_path)
source = ConfigSource.from_base_dir(tmp_path)
# First bootstrap
bootstrapper1 = SettingsBootstrap(source, schema)
bootstrapper1.bootstrap()
env_content_1 = source.env_path.read_text()
# Second bootstrap
bootstrapper2 = SettingsBootstrap(source, schema)
bootstrapper2.bootstrap()
env_content_2 = source.env_path.read_text()
# All secrets should be identical
assert env_content_1 == env_content_2
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
):
"""Test that custom variables added to .env are preserved."""
from alfred.settings_schema import load_schema
schema = load_schema(test_toml_with_all_types)
source = ConfigSource.from_base_dir(test_toml_with_all_types)
# First bootstrap
bootstrapper1 = SettingsBootstrap(source, schema)
bootstrapper1.bootstrap()
# Add custom variables to .env
with open(source.env_path, "a") as f:
f.write("\nMY_CUSTOM_VAR=custom_value\n")
f.write("ANOTHER_CUSTOM=another_value\n")
# Second bootstrap
bootstrapper2 = SettingsBootstrap(source, schema)
bootstrapper2.bootstrap()
env_content = source.env_path.read_text()
# Custom variables should be preserved
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
):
"""Test that custom variables are placed in a dedicated section."""
from alfred.settings_schema import load_schema
schema = load_schema(test_toml_with_all_types)
source = ConfigSource.from_base_dir(test_toml_with_all_types)
# Create .env with custom variable
source.env_path.write_text("MY_CUSTOM_VAR=test\n")
bootstrapper = SettingsBootstrap(source, schema)
bootstrapper.bootstrap()
env_content = source.env_path.read_text()
# Check custom section exists
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
):
"""Test that custom variables in .env.example are preserved."""
from alfred.settings_schema import load_schema
schema = load_schema(test_toml_with_all_types)
source = ConfigSource.from_base_dir(test_toml_with_all_types)
bootstrapper = SettingsBootstrap(source, schema)
bootstrapper.bootstrap()
env_content = source.env_path.read_text()
# CUSTOM_VAR is in .env.example but not in schema
assert "CUSTOM_VAR=custom_value" in env_content
class TestBooleanHandling:
"""Test that booleans are handled correctly."""
def test_booleans_written_as_lowercase(
self, test_toml_with_all_types, test_env_example
):
"""Test that Python booleans are written as lowercase strings."""
from alfred.settings_schema import load_schema
schema = load_schema(test_toml_with_all_types)
source = ConfigSource.from_base_dir(test_toml_with_all_types)
bootstrapper = SettingsBootstrap(source, schema)
bootstrapper.bootstrap()
env_content = source.env_path.read_text()
# Boolean should be lowercase
assert "BOOL_VAR=true" in env_content
assert "BOOL_VAR=True" not in env_content
assert "BOOL_VAR=TRUE" not in env_content
def test_false_boolean_written_as_lowercase(self, tmp_path):
"""Test that False is written as 'false'."""
toml_content = """
[tool.poetry]
name = "test"
version = "1.0.0"
[tool.alfred.settings_schema.BOOL_FALSE]
type = "boolean"
source = "env"
default = false
category = "test"
"""
(tmp_path / "pyproject.toml").write_text(toml_content)
(tmp_path / ".env.example").write_text("BOOL_FALSE=\n")
from alfred.settings_schema import load_schema
schema = load_schema(tmp_path)
source = ConfigSource.from_base_dir(tmp_path)
bootstrapper = SettingsBootstrap(source, schema)
bootstrapper.bootstrap()
env_content = source.env_path.read_text()
assert "BOOL_FALSE=false" in env_content
assert "BOOL_FALSE=False" not in env_content
def test_boolean_parsing_from_env(self, tmp_path):
"""Test that various boolean formats are parsed correctly."""
toml_content = """
[tool.poetry]
name = "test"
version = "1.0.0"
[tool.alfred.settings_schema.BOOL_VAR]
type = "boolean"
source = "env"
default = false
category = "test"
"""
(tmp_path / "pyproject.toml").write_text(toml_content)
(tmp_path / ".env.example").write_text("BOOL_VAR=\n")
from alfred.settings_schema import load_schema
schema = load_schema(tmp_path)
source = ConfigSource.from_base_dir(tmp_path)
# Test different boolean formats
test_cases = [
("true", True),
("TRUE", True),
("True", True),
("1", True),
("yes", True),
("false", False),
("FALSE", False),
("False", False),
("0", False),
("no", False),
]
for input_val, expected in test_cases:
source.env_path.write_text(f"BOOL_VAR={input_val}\n")
bootstrapper = SettingsBootstrap(source, schema)
bootstrapper._load_sources()
bootstrapper._resolve_settings()
assert bootstrapper.resolved_settings["BOOL_VAR"] == expected
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
):
"""Test that computed variables are written with their computed values."""
from alfred.settings_schema import load_schema
schema = load_schema(test_toml_with_all_types)
source = ConfigSource.from_base_dir(test_toml_with_all_types)
bootstrapper = SettingsBootstrap(source, schema)
bootstrapper.bootstrap()
env_content = source.env_path.read_text()
# Computed variable should have its computed value
assert "COMPUTED_VAR=default_string_42" in env_content
def test_computed_uri_example(self, tmp_path):
"""Test computed URI (like MONGO_URI) is written correctly."""
toml_content = """
[tool.poetry]
name = "test"
version = "1.0.0"
[tool.alfred.settings_schema.DB_HOST]
type = "string"
source = "env"
default = "localhost"
category = "database"
[tool.alfred.settings_schema.DB_PORT]
type = "integer"
source = "env"
default = 5432
category = "database"
[tool.alfred.settings_schema.DB_USER]
type = "string"
source = "env"
default = "user"
category = "database"
[tool.alfred.settings_schema.DB_PASSWORD]
type = "secret"
source = "generated"
secret_rule = "16:hex"
category = "security"
[tool.alfred.settings_schema.DB_URI]
type = "computed"
source = "computed"
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 / ".env.example").write_text(
"DB_HOST=\nDB_PORT=\nDB_USER=\nDB_PASSWORD=\nDB_URI=\n"
)
from alfred.settings_schema import load_schema
schema = load_schema(tmp_path)
source = ConfigSource.from_base_dir(tmp_path)
bootstrapper = SettingsBootstrap(source, schema)
bootstrapper.bootstrap()
env_content = source.env_path.read_text()
# Check URI is computed and written
assert "DB_URI=postgresql://user:" in env_content
assert "@localhost:5432/db" in env_content
# Extract password from URI to verify it's the same as DB_PASSWORD
import re
uri_match = re.search(r"DB_URI=postgresql://user:([^@]+)@", env_content)
password_match = re.search(r"DB_PASSWORD=([^\n]+)", env_content)
assert uri_match and password_match
assert uri_match.group(1) == password_match.group(1)
class TestEdgeCases:
"""Test edge cases and error conditions."""
def test_missing_env_example(self, tmp_path):
"""Test that missing .env.example raises error."""
toml_content = """
[tool.poetry]
name = "test"
version = "1.0.0"
[tool.alfred.settings_schema.TEST_VAR]
type = "string"
source = "env"
default = "test"
category = "test"
"""
(tmp_path / "pyproject.toml").write_text(toml_content)
from alfred.settings_schema import load_schema
schema = load_schema(tmp_path)
source = ConfigSource.from_base_dir(tmp_path)
bootstrapper = SettingsBootstrap(source, schema)
with pytest.raises(FileNotFoundError, match=".env.example not found"):
bootstrapper.bootstrap()
def test_empty_env_example(self, tmp_path):
"""Test that empty .env.example works."""
toml_content = """
[tool.poetry]
name = "test"
version = "1.0.0"
[tool.alfred.settings_schema.TEST_VAR]
type = "string"
source = "env"
default = "test"
category = "test"
"""
(tmp_path / "pyproject.toml").write_text(toml_content)
(tmp_path / ".env.example").write_text("")
from alfred.settings_schema import load_schema
schema = load_schema(tmp_path)
source = ConfigSource.from_base_dir(tmp_path)
bootstrapper = SettingsBootstrap(source, schema)
bootstrapper.bootstrap()
# Should create .env even if .env.example is empty
assert source.env_path.exists()
def test_variable_with_equals_in_value(self, tmp_path):
"""Test that variables with '=' in their value are handled correctly."""
toml_content = """
[tool.poetry]
name = "test"
version = "1.0.0"
[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 / ".env.example").write_text("URL_VAR=\n")
from alfred.settings_schema import load_schema
schema = load_schema(tmp_path)
source = ConfigSource.from_base_dir(tmp_path)
bootstrapper = SettingsBootstrap(source, schema)
bootstrapper.bootstrap()
env_content = source.env_path.read_text()
assert "URL_VAR=http://example.com?key=value" in env_content
def test_preserves_existing_values_on_update(self, tmp_path):
"""Test that existing values are preserved when updating."""
toml_content = """
[tool.poetry]
name = "test"
version = "1.0.0"
[tool.alfred.settings_schema.VAR1]
type = "string"
source = "env"
default = "default1"
category = "test"
[tool.alfred.settings_schema.VAR2]
type = "string"
source = "env"
default = "default2"
category = "test"
"""
(tmp_path / "pyproject.toml").write_text(toml_content)
(tmp_path / ".env.example").write_text("VAR1=\nVAR2=\n")
from alfred.settings_schema import load_schema
schema = load_schema(tmp_path)
source = ConfigSource.from_base_dir(tmp_path)
# First bootstrap
bootstrapper1 = SettingsBootstrap(source, schema)
bootstrapper1.bootstrap()
# Modify values
source.env_path.write_text("VAR1=custom1\nVAR2=custom2\n")
# Second bootstrap
bootstrapper2 = SettingsBootstrap(source, schema)
bootstrapper2.bootstrap()
env_content = source.env_path.read_text()
# Custom values should be preserved
assert "VAR1=custom1" in env_content
assert "VAR2=custom2" in env_content
class TestIntegration:
"""Integration tests with realistic scenarios."""
def test_full_workflow_like_alfred(self, tmp_path):
"""Test a full workflow similar to Alfred's actual usage."""
toml_content = """
[tool.poetry]
name = "alfred"
version = "0.1.7"
[tool.poetry.dependencies]
python = "^3.14"
[tool.alfred.settings_schema.ALFRED_VERSION]
type = "string"
source = "toml"
toml_path = "tool.poetry.version"
category = "build"
export_to_env_make = true
[tool.alfred.settings_schema.HOST]
type = "string"
source = "env"
default = "0.0.0.0"
category = "app"
[tool.alfred.settings_schema.PORT]
type = "integer"
source = "env"
default = 3080
category = "app"
[tool.alfred.settings_schema.JWT_SECRET]
type = "secret"
source = "generated"
secret_rule = "32:hex"
category = "security"
[tool.alfred.settings_schema.MONGO_HOST]
type = "string"
source = "env"
default = "mongodb"
category = "database"
[tool.alfred.settings_schema.MONGO_PASSWORD]
type = "secret"
source = "generated"
secret_rule = "16:hex"
category = "security"
[tool.alfred.settings_schema.MONGO_URI]
type = "computed"
source = "computed"
compute_from = ["MONGO_HOST", "MONGO_PASSWORD"]
compute_template = "mongodb://user:{MONGO_PASSWORD}@{MONGO_HOST}:27017/db"
category = "database"
[tool.alfred.settings_schema.DEBUG_MODE]
type = "boolean"
source = "env"
default = false
category = "app"
"""
(tmp_path / "pyproject.toml").write_text(toml_content)
env_example_content = """# Application settings
HOST=0.0.0.0
PORT=3080
DEBUG_MODE=false
# Security
JWT_SECRET=
# Database
MONGO_HOST=mongodb
MONGO_PASSWORD=
MONGO_URI=
# Build info
ALFRED_VERSION=
"""
(tmp_path / ".env.example").write_text(env_example_content)
from alfred.settings_schema import load_schema
schema = load_schema(tmp_path)
source = ConfigSource.from_base_dir(tmp_path)
# First bootstrap
bootstrapper1 = SettingsBootstrap(source, schema)
bootstrapper1.bootstrap()
env_content_1 = source.env_path.read_text()
# Verify structure
assert "# Application settings" in env_content_1
assert "HOST=0.0.0.0" in env_content_1
assert "PORT=3080" in env_content_1
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 "MONGO_URI=mongodb://user:" in env_content_1
# Second bootstrap - should preserve everything
bootstrapper2 = SettingsBootstrap(source, schema)
bootstrapper2.bootstrap()
env_content_2 = source.env_path.read_text()
# Everything should be identical
assert env_content_1 == env_content_2
# Add custom variable
with open(source.env_path, "a") as f:
f.write("\nMY_CUSTOM_SETTING=test123\n")
# Third bootstrap - should preserve custom
bootstrapper3 = SettingsBootstrap(source, schema)
bootstrapper3.bootstrap()
env_content_3 = source.env_path.read_text()
assert "MY_CUSTOM_SETTING=test123" in env_content_3
assert "# --- CUSTOM VARIABLES ---" in env_content_3