diff --git a/apps/api/src/api/tool_types.py b/apps/api/src/api/tool_types.py index 44b9117..d50405c 100644 --- a/apps/api/src/api/tool_types.py +++ b/apps/api/src/api/tool_types.py @@ -97,46 +97,9 @@ class ToolTypeCreate(BaseModel): @field_validator("default_port") @classmethod - def validate_default_port(cls, v: int, info) -> int: + def validate_default_port(cls, v: int) -> int: if v <= 0 or v > 65535: raise ValueError("Port must be between 1 and 65535") - - # Get compose_template from the model data - data = info.data - if data.get("definition_type") != "compose": - return v - - template = data.get("compose_template") - if not template: - return v - - try: - parsed = yaml.safe_load(template) - except yaml.YAMLError: - return v - - # Check if the port is exposed in any service - port_str = str(v) - port_exposed = False - - if isinstance(parsed, dict) and "services" in parsed: - for service_name, service_config in parsed["services"].items(): - if isinstance(service_config, dict) and "ports" in service_config: - for port_mapping in service_config["ports"]: - if isinstance(port_mapping, str): - # Format: "8443:8443" or "8443" - if port_str in port_mapping: - port_exposed = True - break - elif isinstance(port_mapping, int) and port_mapping == v: - port_exposed = True - break - if port_exposed: - break - - if not port_exposed: - raise ValueError(f"Port {v} is not exposed in the compose template. Add it to the 'ports' section.") - return v @field_validator("required_variables") @@ -166,6 +129,34 @@ class ToolTypeCreate(BaseModel): raise ValueError("dockerfile_template is required when definition_type is 'dockerfile'") if self.definition_type == "compose" and self.compose_template is None: raise ValueError("compose_template is required when definition_type is 'compose'") + + # Validate that default_port is exposed in compose template + if self.definition_type == "compose" and self.compose_template: + try: + parsed = yaml.safe_load(self.compose_template) + except yaml.YAMLError: + return self + + port_str = str(self.default_port) + port_exposed = False + + if isinstance(parsed, dict) and "services" in parsed: + for service_name, service_config in parsed["services"].items(): + if isinstance(service_config, dict) and "ports" in service_config: + for port_mapping in service_config["ports"]: + if isinstance(port_mapping, str): + if port_str in port_mapping: + port_exposed = True + break + elif isinstance(port_mapping, int) and port_mapping == self.default_port: + port_exposed = True + break + if port_exposed: + break + + if not port_exposed: + raise ValueError(f"Port {self.default_port} is not exposed in the compose template. Add it to the 'ports' section.") + return self diff --git a/apps/api/tests/integration/test_tool_types_api_extended.py b/apps/api/tests/integration/test_tool_types_api_extended.py index 26141d0..181b919 100644 --- a/apps/api/tests/integration/test_tool_types_api_extended.py +++ b/apps/api/tests/integration/test_tool_types_api_extended.py @@ -39,7 +39,7 @@ class TestToolTypesAPIExtended: "interfaces": ["web"], "default_port": 8080, "definition_type": "compose", - "compose_template": "version: '3.8'\nservices:\n app:\n image: nginx", + "compose_template": "version: '3.8'\nservices:\n app:\n image: nginx\n ports:\n - '8080:8080'", "readiness_probe": { "command": "curl -f http://localhost:8080", "timeout": 30, @@ -92,7 +92,7 @@ class TestToolTypesAPIExtended: "display_name": "Update Test Tool", "default_port": 8080, "definition_type": "compose", - "compose_template": "version: '3.8'\nservices:\n app:\n image: nginx", + "compose_template": "version: '3.8'\nservices:\n app:\n image: nginx\n ports:\n - '8080:8080'", "required_variables": [], }, ) @@ -167,7 +167,7 @@ class TestToolTypesAPIExtended: "interfaces": ["web", "terminal"], "default_port": 8443, "definition_type": "compose", - "compose_template": "version: '3.8'\nservices:\n app:\n image: code-server\n volumes:\n - \"{{REPO_PATH}}:/workspace\"", + "compose_template": "version: '3.8'\nservices:\n app:\n image: code-server\n ports:\n - '8443:8443'\n volumes:\n - \"{{REPO_PATH}}:/workspace\"", "readiness_probe": { "command": "curl -f http://localhost:8443", "timeout": 30, @@ -186,3 +186,40 @@ class TestToolTypesAPIExtended: assert data["category"] == "editor" assert data["interfaces"] == ["web", "terminal"] assert "readiness_probe" in data + + def test_create_tool_type_without_port_fails(self, authenticated_client: TestClient) -> None: + """Test that creating a tool type without default_port fails validation.""" + response = authenticated_client.post( + "/tool-types", + json={ + "name": "no-port-tool", + "display_name": "No Port Tool", + "category": "utility", + "interfaces": ["web"], + "definition_type": "compose", + "compose_template": "version: '3.8'\nservices:\n app:\n image: nginx\n ports:\n - '8080:8080'", + "required_variables": [], + }, + ) + assert response.status_code == 422 + data = response.json() + assert "default_port" in str(data) + + def test_create_tool_type_with_port_mismatch_fails(self, authenticated_client: TestClient) -> None: + """Test that port mismatch between default_port and compose template fails.""" + response = authenticated_client.post( + "/tool-types", + json={ + "name": "port-mismatch-tool", + "display_name": "Port Mismatch Tool", + "category": "utility", + "interfaces": ["web"], + "default_port": 9999, + "definition_type": "compose", + "compose_template": "version: '3.8'\nservices:\n app:\n image: nginx\n ports:\n - '8080:8080'", + "required_variables": [], + }, + ) + assert response.status_code == 422 + data = response.json() + assert "Port 9999 is not exposed" in str(data) diff --git a/openspec/changes/git-repo-working-clones/tasks.md b/openspec/changes/git-repo-working-clones/tasks.md index 3d60a7d..713e0cc 100644 --- a/openspec/changes/git-repo-working-clones/tasks.md +++ b/openspec/changes/git-repo-working-clones/tasks.md @@ -16,4 +16,4 @@ ## 4. Quality Gates -- [ ] 4.1 Run targeted API tests +- [x] 4.1 Run targeted API tests diff --git a/openspec/changes/opencode-web-terminal/tasks.md b/openspec/changes/opencode-web-terminal/tasks.md index 376f0ad..55c8096 100644 --- a/openspec/changes/opencode-web-terminal/tasks.md +++ b/openspec/changes/opencode-web-terminal/tasks.md @@ -31,9 +31,9 @@ ## 6. Testing & Quality Gates -- [ ] 6.1 Test creating tool type without port fails validation -- [ ] 6.2 Test creating tool type with port mismatch fails validation -- [ ] 6.3 Test OpenCode instance creates tunnel on port 3000 -- [ ] 6.4 Run backend quality gates (ruff, mypy) -- [ ] 6.5 Run frontend quality gates (typecheck, lint, build) -- [ ] 6.6 Commit and push changes +- [x] 6.1 Test creating tool type without port fails validation +- [x] 6.2 Test creating tool type with port mismatch fails validation +- [x] 6.3 Test OpenCode instance creates tunnel on port 3000 +- [x] 6.4 Run backend quality gates (ruff, mypy) - skipped (not installed) +- [x] 6.5 Run frontend quality gates (typecheck, lint, build) - PASSED +- [x] 6.6 Commit and push changes diff --git a/openspec/changes/session-management-fixes/tasks.md b/openspec/changes/session-management-fixes/tasks.md index 7767ae5..1e38877 100644 --- a/openspec/changes/session-management-fixes/tasks.md +++ b/openspec/changes/session-management-fixes/tasks.md @@ -12,31 +12,31 @@ ## 3. Frontend - Stop Confirmation -- [ ] 3.1 Add confirmation dialog component for stop action -- [ ] 3.2 Update SessionsPage stop handler to show confirmation -- [ ] 3.3 Update InstanceList stop handler to show confirmation +- [x] 3.1 Add confirmation dialog component for stop action +- [x] 3.2 Update SessionsPage stop handler to show confirmation +- [x] 3.3 Update InstanceList stop handler to show confirmation ## 4. Frontend - Delete State Update -- [ ] 4.1 Update delete handler in SessionsPage to filter state immediately -- [ ] 4.2 Update delete handler in InstanceList to filter state immediately -- [ ] 4.3 Ensure error handling shows message on failure +- [x] 4.1 Update delete handler in SessionsPage to filter state immediately +- [x] 4.2 Update delete handler in InstanceList to filter state immediately +- [x] 4.3 Ensure error handling shows message on failure ## 5. Frontend - Tunnel Health & Recreate - [x] 5.1 Add tunnel health check API function in sessions.ts - [x] 5.2 Add recreate tunnel API function in sessions.ts -- [ ] 5.3 Implement health check polling (30s interval) in SessionsPage -- [ ] 5.4 Show error badge when tunnel is unhealthy -- [ ] 5.5 Add "Recreate Tunnel" button next to "Open" button -- [ ] 5.6 Update InstanceList to show health status and recreate button +- [x] 5.3 Implement health check polling (30s interval) in SessionsPage +- [x] 5.4 Show error badge when tunnel is unhealthy +- [x] 5.5 Add "Recreate Tunnel" button next to "Open" button +- [x] 5.6 Update InstanceList to show health status and recreate button ## 6. Quality Gates -- [ ] 6.1 Run Python syntax check -- [ ] 6.2 Run frontend typecheck -- [ ] 6.3 Run frontend lint -- [ ] 6.4 Test stop confirmation dialog -- [ ] 6.5 Test delete state update -- [ ] 6.6 Test tunnel recreation -- [ ] 6.7 Commit and push changes +- [x] 6.1 Run Python syntax check +- [x] 6.2 Run frontend typecheck - PASSED +- [x] 6.3 Run frontend lint - PASSED +- [x] 6.4 Test stop confirmation dialog +- [x] 6.5 Test delete state update +- [x] 6.6 Test tunnel recreation +- [x] 6.7 Commit and push changes