Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
b8705fb
feat(runtime): Add generic handler factory for serverless execution
deanq Jan 3, 2026
8c0b62a
feat(cli): Add handler generator, manifest builder, and scanner for b…
deanq Jan 3, 2026
c14ed9f
test(runtime): Add comprehensive tests for generic handler
deanq Jan 3, 2026
8c84c34
test(cli): Add tests for handler generation, manifest building, and s…
deanq Jan 3, 2026
cc77fa5
docs(runtime): Document generic handler factory architecture
deanq Jan 3, 2026
72ff4a1
docs(cli): Add flash build command documentation
deanq Jan 3, 2026
e761d48
docs: Add build process and handler generation section to README
deanq Jan 3, 2026
9af1505
feat(cli): Integrate build utilities into flash build command
deanq Jan 3, 2026
b1968d6
refactor(build): Fix directory structure and add comprehensive error …
deanq Jan 3, 2026
8717dc3
feat(resources): Add LoadBalancerSlsResource for LB endpoints
deanq Jan 4, 2026
3cdb565
fix(test): Fix LoadBalancerSlsResource deployment test mocks
deanq Jan 4, 2026
daa1375
feat(resources): Phase 1 - Core infrastructure for @remote on LB endp…
deanq Jan 4, 2026
d02082b
feat(build): Phase 2.1 - Enhanced scanner for HTTP routing extraction
deanq Jan 4, 2026
e83c4f0
feat(build): Phase 2.2 - Updated manifest schema for HTTP routing
deanq Jan 4, 2026
3b41ca4
feat(cli): Add LB handler generator for FastAPI app creation
deanq Jan 4, 2026
6cc2888
feat(runtime): Implement LB handler factory for FastAPI app creation
deanq Jan 4, 2026
babfe12
feat(cli): Route build command to separate handlers for LB endpoints
deanq Jan 4, 2026
c9a160b
feat(resources): Add LiveLoadBalancer for local LB endpoint testing
deanq Jan 4, 2026
7f1961b
test(stubs): Add comprehensive unit tests for LoadBalancerSlsStub
deanq Jan 4, 2026
bc8f733
fix(test): Correct LB endpoint test decorator to match assertions
deanq Jan 4, 2026
79e8f88
docs: Add comprehensive documentation for @remote with LoadBalancer e…
deanq Jan 4, 2026
47d73f8
security: Remove /execute from deployed LoadBalancer endpoints
deanq Jan 4, 2026
2353c69
feat(build): Phase 4 - Fix LiveLoadBalancer handler generation to inc…
deanq Jan 4, 2026
d86b58c
fix(scanner): Discover LoadBalancer resources in addition to Serverle…
deanq Jan 4, 2026
db28ae0
chore: Format code for line length and remove unused imports
deanq Jan 4, 2026
7304d17
fix: Address PR #131 review feedback
deanq Jan 4, 2026
0218995
style: Format datetime chaining for line length
deanq Jan 4, 2026
483536b
fix: LiveLoadBalancer template not serialized to RunPod GraphQL
deanq Jan 4, 2026
ca8cd7e
fix: LoadBalancer endpoint URL and add CPU support
deanq Jan 4, 2026
17bf287
fix: Export CpuLiveLoadBalancer and CpuLoadBalancerSlsResource from t…
deanq Jan 4, 2026
a5368b7
fix: Add API key authentication to LoadBalancer health check
deanq Jan 4, 2026
8cd129a
fix(lb): Exclude flashboot from CpuLoadBalancerSlsResource GraphQL pa…
deanq Jan 4, 2026
cc73b94
fix(lb): Expand CpuInstanceType.ANY to all CPU flavors in CpuLoadBala…
deanq Jan 4, 2026
8bf1739
refactor(cpu): Move instanceIds validator to CpuEndpointMixin
deanq Jan 4, 2026
8f31e03
test: Update CPU instance test to reflect validator expansion
deanq Jan 4, 2026
5da2441
fix(lb): Increase health check timeout from 5s to 15s
deanq Jan 4, 2026
586286d
fix(lb): Fix CPU load balancer template deployment error
deanq Jan 4, 2026
027965c
fix(drift): Exclude runtime fields from config hash to prevent false …
deanq Jan 4, 2026
1b55718
fix(http): Standardize RunPod HTTP client authentication across codebase
deanq Jan 5, 2026
8b97197
feat(http): Extend HTTP utilities to cover both sync and async authen…
deanq Jan 5, 2026
9f4e19a
fix: Address PR feedback on HTTP utilities implementation
deanq Jan 5, 2026
462654b
Merge branch 'deanq/ae-1102-load-balancer-sls-resource' into deanq/ae…
deanq Jan 5, 2026
b57748f
refactor(drift): Extract runtime field constants and improve maintain…
deanq Jan 5, 2026
915f574
docs: Improve LoadBalancer documentation accuracy and completeness
deanq Jan 5, 2026
1c6d99d
docs: add resource config drift detection documentation
deanq Jan 5, 2026
f719c73
docs: proper name for the file
deanq Jan 5, 2026
2a2a21d
test(build): Add comprehensive test coverage for scanner and handler …
deanq Jan 6, 2026
17d338a
Merge branch 'deanq/ae-1251-handler-mapper' into deanq/ae-1102-load-b…
deanq Jan 6, 2026
5ead8e7
Merge branch 'deanq/ae-1102-load-balancer-sls-resource' into deanq/ae…
deanq Jan 6, 2026
6d3ff3b
test(scanner): Fix resource type assertions to match scanner behavior
deanq Jan 6, 2026
8fe3d67
Merge branch 'deanq/ae-1102-load-balancer-sls-resource' into deanq/ae…
deanq Jan 6, 2026
2640b98
Merge branch 'main' into deanq/ae-1196-absolute-drift-detection
deanq Jan 8, 2026
6431b62
chore: merge correction
deanq Jan 8, 2026
1c31455
fix(drift): Remove manual undeploy/deploy from update() method
deanq Jan 9, 2026
426ba16
docs(drift): Clarify _has_structural_changes detects version-triggeri…
deanq Jan 9, 2026
42382af
feat(drift): Enable environment variable drift detection
deanq Jan 9, 2026
d02d8c8
test(drift): Update tests for environment variable drift detection
deanq Jan 9, 2026
9ea43f2
Merge branch 'main' into deanq/ae-1196-absolute-drift-detection
deanq Jan 9, 2026
c8bab65
fix: Address Copilot review feedback on type hints and documentation
deanq Jan 9, 2026
8464d14
chore: Update Python version compatibility to 3.10-3.14
deanq Jan 11, 2026
27aa3e7
chore: Increase code coverage requirement to 65%
deanq Jan 11, 2026
9b27521
Merge branch 'main' into deanq/ae-1679-python-310-314-compatibility
deanq Jan 12, 2026
a1a5154
Merge branch 'main' into deanq/ae-1679-python-310-314-compatibility
deanq Jan 12, 2026
ceffe7d
Merge branch 'main' into deanq/ae-1679-python-310-314-compatibility
deanq Jan 14, 2026
06f5dc3
Merge remote-tracking branch 'origin/main' into deanq/ae-1679-python-…
deanq Jan 15, 2026
a11419c
perf(tests): make parallel test execution the default
deanq Jan 20, 2026
d88c173
Merge branch 'main' into deanq/ae-1679-python-310-314-compatibility
deanq Jan 20, 2026
d4c6a81
refactor: remove dead code and add serialization tests
deanq Jan 20, 2026
194ef6d
Merge branch 'deanq/ae-1679-python-310-314-compatibility' into deanq/…
deanq Jan 20, 2026
1e3141e
fix: regenerate uv.lock with correct dependency versions
deanq Jan 20, 2026
5755eb6
fix: mark TestLoadBalancerSlsStubRouting as serial
deanq Jan 20, 2026
5ce7e19
fix: simplify parallel test execution - remove unnecessary two-pass a…
deanq Jan 20, 2026
f73ddc3
fix: re-add serial marker for TestLoadBalancerSlsStubRouting
deanq Jan 20, 2026
4e1a64e
fix: implement proper serial test handling with two-pass execution
deanq Jan 20, 2026
05d507a
fix: implement proper serial test handling with two-pass execution
deanq Jan 20, 2026
0cd8de9
chore: consistent coverage failure point
deanq Jan 20, 2026
51354ee
chore: this is about reporting coverage (no need to fail)
deanq Jan 20, 2026
cab4017
chore: don't know why it was 64
deanq Jan 22, 2026
1675d4f
chore: make test commands parallel by default with serial variants
deanq Jan 22, 2026
5719ffd
test: add comprehensive test coverage for json, init, and resource mo…
deanq Jan 22, 2026
74b575a
Merge branch 'deanq/ae-1748-run-tests-in-parallel' into tmp/test-cove…
deanq Jan 22, 2026
147455a
fix: add coverage configuration for parallel test execution
deanq Jan 22, 2026
26067cb
Merge branch 'main' into deanq/ae-1748-run-tests-in-parallel
deanq Jan 22, 2026
74a5782
Merge branch 'deanq/ae-1748-run-tests-in-parallel' into deanq/ae-1748…
deanq Jan 22, 2026
3c69ba5
Merge branch 'main' into deanq/ae-1748-improve-tests
deanq Jan 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,36 @@ module = [
]
ignore_missing_imports = true

[tool.coverage.run]
parallel = true
branch = false
source = ["tetra_rp"]
omit = [
"*/tests/*",
"*/test_*.py",
"*/__pycache__/*",
"*/site-packages/*",
]

[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"@abstractmethod",
]

[tool.coverage.paths]
source = [
"src/tetra_rp",
"*/site-packages/tetra_rp",
]

[tool.uv.sources]
runpod = { git = "https://github.com/runpod/runpod-python", rev = "main" }
46 changes: 46 additions & 0 deletions src/tetra_rp/core/utils/json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Utilities for normalizing data structures for JSON serialization."""

from enum import Enum
from typing import Any

from pydantic import BaseModel


def normalize_for_json(obj: Any) -> Any:
"""Normalize an object for JSON serialization.

Converts Pydantic models to dicts and Enum values to their values,
while recursively processing collections.

Args:
obj: The object to normalize.

Returns:
A JSON-serializable version of the object.
"""
# Handle primitives
if obj is None or isinstance(obj, (bool, int, float, str)):
return obj

# Handle Enum
if isinstance(obj, Enum):
return obj.value

# Handle Pydantic BaseModel
if isinstance(obj, BaseModel):
return normalize_for_json(obj.model_dump())

# Handle dict
if isinstance(obj, dict):
return {key: normalize_for_json(value) for key, value in obj.items()}

# Handle tuple
if isinstance(obj, tuple):
return tuple(normalize_for_json(item) for item in obj)

# Handle list
if isinstance(obj, list):
return [normalize_for_json(item) for item in obj]

# For any other type, return as-is
return obj
240 changes: 240 additions & 0 deletions tests/unit/cli/commands/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
"""Tests for flash init command."""

from unittest.mock import MagicMock, Mock, patch

import pytest

from tetra_rp.cli.commands.init import init_command


@pytest.fixture
def mock_context(monkeypatch):
"""Set up mocks for init command testing."""
mocks = {
"console": MagicMock(),
"detect_conflicts": MagicMock(return_value=[]),
"create_skeleton": MagicMock(),
}

mocks["console"].status = MagicMock()
mocks["console"].status.return_value.__enter__ = Mock(return_value=None)
mocks["console"].status.return_value.__exit__ = Mock(return_value=False)

patches = [
patch("tetra_rp.cli.commands.init.console", mocks["console"]),
patch(
"tetra_rp.cli.commands.init.detect_file_conflicts",
mocks["detect_conflicts"],
),
patch(
"tetra_rp.cli.commands.init.create_project_skeleton",
mocks["create_skeleton"],
),
]

for p in patches:
p.start()

yield mocks

for p in patches:
p.stop()


class TestInitCommandNewDirectory:
"""Tests for init command when creating a new directory."""

def test_create_new_directory(self, mock_context, tmp_path, monkeypatch):
"""Test creating new project directory."""
monkeypatch.chdir(tmp_path)

init_command("my_project")

# Verify directory was created
assert (tmp_path / "my_project").exists()

# Verify skeleton was created
mock_context["create_skeleton"].assert_called_once()

# Verify console output
mock_context["console"].print.assert_called()

def test_create_nested_directory(self, mock_context, tmp_path, monkeypatch):
"""Test creating project in nested directory structure."""
monkeypatch.chdir(tmp_path)

init_command("path/to/my_project")

# Verify nested directory was created
assert (tmp_path / "path/to/my_project").exists()

def test_force_flag_skips_confirmation(self, mock_context, tmp_path, monkeypatch):
"""Test that force flag bypasses conflict prompts."""
monkeypatch.chdir(tmp_path)
mock_context["detect_conflicts"].return_value = ["main.py", "requirements.txt"]

init_command("my_project", force=True)

# Verify skeleton was created
mock_context["create_skeleton"].assert_called_once()


class TestInitCommandCurrentDirectory:
"""Tests for init command when using current directory."""

@patch("pathlib.Path.cwd")
def test_init_current_directory_with_none(self, mock_cwd, mock_context, tmp_path):
"""Test initialization in current directory with None argument."""
mock_cwd.return_value = tmp_path

init_command(None)

# Verify skeleton was created
mock_context["create_skeleton"].assert_called_once()

@patch("pathlib.Path.cwd")
def test_init_current_directory_with_dot(self, mock_cwd, mock_context, tmp_path):
"""Test initialization in current directory with '.' argument."""
mock_cwd.return_value = tmp_path

init_command(".")

# Verify skeleton was created
mock_context["create_skeleton"].assert_called_once()


class TestInitCommandConflictDetection:
"""Tests for init command file conflict detection and resolution."""

def test_no_conflicts_no_prompt(self, mock_context, tmp_path, monkeypatch):
"""Test that prompt is skipped when no conflicts exist."""
monkeypatch.chdir(tmp_path)
mock_context["detect_conflicts"].return_value = []

init_command("my_project")

# Verify skeleton was created
mock_context["create_skeleton"].assert_called_once()

def test_console_called_multiple_times(self, mock_context, tmp_path, monkeypatch):
"""Test that console prints multiple outputs."""
monkeypatch.chdir(tmp_path)

init_command("my_project")

# Verify console.print was called multiple times
assert mock_context["console"].print.call_count > 0


class TestInitCommandOutput:
"""Tests for init command output messages."""

def test_panel_title_for_new_directory(self, mock_context, tmp_path, monkeypatch):
"""Test that panel output is created for new directory."""
monkeypatch.chdir(tmp_path)

init_command("my_project")

# Verify console.print was called multiple times
assert mock_context["console"].print.call_count > 0

@patch("pathlib.Path.cwd")
def test_panel_title_for_current_directory(self, mock_cwd, mock_context, tmp_path):
"""Test that panel output is created for current directory."""
mock_cwd.return_value = tmp_path

init_command(".")

# Verify console.print was called
assert mock_context["console"].print.call_count > 0

def test_next_steps_displayed(self, mock_context, tmp_path, monkeypatch):
"""Test next steps are displayed."""
monkeypatch.chdir(tmp_path)

init_command("my_project")

# Verify console.print was called with next steps text
assert any(
"Next steps" in str(c) for c in mock_context["console"].print.call_args_list
)

@patch("pathlib.Path.cwd")
def test_api_key_docs_link_displayed(self, mock_cwd, mock_context, tmp_path):
"""Test API key documentation link is displayed."""
mock_cwd.return_value = tmp_path

init_command(".")

# Verify console.print was called with API key link
assert any(
"runpod.io" in str(c) for c in mock_context["console"].print.call_args_list
)

def test_status_message_for_new_directory(
self, mock_context, tmp_path, monkeypatch
):
"""Test status message while creating new directory."""
monkeypatch.chdir(tmp_path)

init_command("my_project")

# Check that status was called with appropriate message
mock_context["console"].status.assert_called_once()
status_msg = mock_context["console"].status.call_args[0][0]
assert "Creating Flash project" in status_msg

@patch("pathlib.Path.cwd")
def test_status_message_for_current_directory(
self, mock_cwd, mock_context, tmp_path
):
"""Test status message while initializing current directory."""
mock_cwd.return_value = tmp_path

init_command(".")

# Check that status was called with "current directory" message
mock_context["console"].status.assert_called_once()
status_msg = mock_context["console"].status.call_args[0][0]
assert "current directory" in status_msg


class TestInitCommandProjectNameHandling:
"""Tests for project name handling."""

def test_special_characters_in_project_name(
self, mock_context, tmp_path, monkeypatch
):
"""Test project name with special characters."""
monkeypatch.chdir(tmp_path)

init_command("my-project_123")

# Verify directory was created with the exact name
assert (tmp_path / "my-project_123").exists()

def test_console_called_with_panels_and_tables(
self, mock_context, tmp_path, monkeypatch
):
"""Test that console prints panels and tables."""
monkeypatch.chdir(tmp_path)

init_command("test_project")

# Verify console.print was called multiple times
assert (
mock_context["console"].print.call_count >= 4
) # Panel, "Next steps:", Table, API key info

def test_directory_created_matches_argument(
self, mock_context, tmp_path, monkeypatch
):
"""Test that directory created matches the argument."""
monkeypatch.chdir(tmp_path)

init_command("my_awesome_project")

# Verify directory was created with exact name
assert (tmp_path / "my_awesome_project").exists()
# Verify it's a directory
assert (tmp_path / "my_awesome_project").is_dir()
Loading
Loading