diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8a368f..6435c4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: ci +name: CI on: pull_request: @@ -6,25 +6,57 @@ on: branches: [main, dev] jobs: - test: + test-python: + name: Python Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" - - name: Install Python deps + + - name: Install dependencies run: | python -m pip install --upgrade pip - pip install fastapi uvicorn pytest - - name: Run unit tests (placeholder) - run: | - pytest || true - - name: Terraform fmt/validate (placeholder) - run: | - echo "terraform fmt/validate would run here" - - name: Secret scan (placeholder) + pip install -r requirements.txt + + - name: Run Python tests + run: pytest tests/ -v --ignore=tests/test_kestra.js --ignore=tests/test-all-tools.mjs + + test-mcp-server: + name: MCP Server Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install and build MCP server + working-directory: infoundry-mcp-server run: | - echo "gitleaks/trufflehog would run here" + npm ci + npm run build + + lint-ui: + name: UI Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install UI dependencies + working-directory: ui + run: npm ci + - name: Run linter + working-directory: ui + run: npm run lint || true # Don't fail on lint warnings diff --git a/.gitignore b/.gitignore index 26d0b60..86e0c5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ __pycache__/ .pytest_cache/ .venv/ +.test_venv/ +venv/ env/ *.pyc .DS_Store @@ -11,3 +13,20 @@ build/ *.tfstate* /service_profile.json +# Logs +*.log +npm-debug.log* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Test coverage +coverage/ +htmlcov/ +.coverage + +# Compiled files +*.egg-info/ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 197ccd5..1a43200 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -17,7 +17,7 @@ Maintainers are responsible for clarifying standards of acceptable behavior and This Code of Conduct applies within all project spaces and in public spaces when an individual is representing the project or its community. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the maintainers at project@cloudgenesis.invalid. All complaints will be reviewed and investigated promptly. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project maintainers. All complaints will be reviewed and investigated promptly. ## Attribution This Code of Conduct is adapted from the Contributor Covenant, version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38268f3..a41027c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,32 +1,32 @@ -# Contributing to CloudGenesis +# Contributing to InFoundry Thank you for considering a contribution! This guide keeps contributions consistent and reviewable. -## Ground rules +## Ground Rules - Use Conventional Commits (feat, fix, chore, docs, refactor). - Keep PRs small and focused; include tests and docs when applicable. - Run linters/tests before pushing. - Follow the Code of Conduct. -## How to contribute +## How to Contribute 1. Fork the repo and create a feature branch: `git checkout -b feat/`. 2. Implement your change with accompanying tests and docs. -3. Run checks (see `ci/test.yml` guidance). +3. Run tests: `pytest tests/ -v`. 4. Submit a PR. Fill the template with context, testing evidence, and risk notes. 5. A maintainer and CodeRabbit must approve before merge. -## Development environment +## Development Environment - Python 3.11+, Node 18+. - Optional: Docker, kind, localstack. -- Install deps per `requirements.txt` (placeholder) and `ui/package.json` when available. +- Install deps: `pip install -r requirements.txt` and `cd ui && npm install`. -## Issues and discussions +## Issues and Discussions - Use issue templates for bugs and features. - Propose substantial design changes in a discussion or an RFC under `docs/`. ## CodeRabbit Review Guidelines -All pull requests are automatically reviewed by [CodeRabbit](https://coderabbit.ai), our AI-powered code review assistant. Here's how to work with CodeRabbit effectively: +All pull requests are automatically reviewed by [CodeRabbit](https://coderabbit.ai), our AI-powered code review assistant. ### How It Works 1. **Automatic Review**: When you open a PR, CodeRabbit automatically analyzes your changes and posts review comments. @@ -36,7 +36,7 @@ All pull requests are automatically reviewed by [CodeRabbit](https://coderabbit. ### Interacting with CodeRabbit - **Ask Questions**: Reply to any CodeRabbit comment with `@coderabbitai` to get clarification. - **Request Re-review**: After making changes, comment `@coderabbitai review` to trigger a fresh review. -- **Dismiss Suggestions**: If a suggestion doesn't apply, explain why in a reply—this helps the bot learn. +- **Dismiss Suggestions**: If a suggestion doesn't apply, explain why in a reply. - **Generate Summaries**: Use `@coderabbitai summary` to get an overview of large PRs. ### Addressing Feedback @@ -57,4 +57,3 @@ A PR can only be merged when: - [ ] All critical issues are resolved - [ ] IaC validation passes (for infrastructure changes) - [ ] At least one maintainer approves - diff --git a/LICENSE b/LICENSE index 65cd917..8b3f539 100644 --- a/LICENSE +++ b/LICENSE @@ -1,177 +1,21 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - +MIT License + +Copyright (c) 2025 InFoundry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 507ba79..1c27ea8 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,85 @@ -# CloudGenesis +# InFoundry -CloudGenesis is a self-adaptive Cloud Architect + SRE agent that inspects a codebase and telemetry, proposes deployable IaC/CI changes, runs safe test deployments, and iteratively optimizes cost, latency, and reliability with human-in-the-loop approvals. +InFoundry is a self-adaptive Cloud Architect + SRE agent that inspects codebases and telemetry, proposes deployable IaC/CI changes, runs safe test deployments, and iteratively optimizes cost, latency, and reliability with human-in-the-loop approvals. -## Quick start -- Install Python 3.11+ and Node 18+. Optional: Docker, kind, localstack. -- Clone the repo, then: `pip install -r requirements.txt` (placeholder) and `npm install` inside `ui/` when it exists. -- Review `docs/BLUEPRINT.md` for the full architecture, plan, and implementation notes. -- Inspect sample telemetry under `examples/telemetry` and run the repo analyzer stub: `python orchestrator/repo_analyzer/scan.py`. -- Explore Kestra pipeline example at `orchestrator/kestra_pipelines/collect-and-summarize.yaml`. +## Features -## Structure -See `docs/BLUEPRINT.md` for the detailed structure and day-by-day plan. The scaffolded folders align with the requested layout (infra, services, orchestrator, ui, tests, examples, scripts, docs, .github/workflows). +- **AI-Powered Architecture Recommendations** - Uses fine-tuned Oumi model for intelligent cloud architecture decisions +- **Kestra Pipeline Orchestration** - End-to-end workflow from repo analysis to PR creation +- **MCP Server for Cline Integration** - Expose all tools via Model Context Protocol +- **React UI Dashboard** - Visual architecture editor with React Flow +- **IaC Generation** - Automatic Terraform generation for AWS -## Licensing -Released under Apache-2.0 (see `LICENSE`). +## Quick Start +### Prerequisites +- Python 3.11+ +- Node.js 18+ +- Optional: Docker, kind, localstack + +### Installation + +```bash +# Clone the repository +git clone https://github.com/your-org/infoundry.git +cd infoundry + +# Install Python dependencies +pip install -r requirements.txt + +# Install UI dependencies +cd ui && npm install && cd .. + +# Install MCP server dependencies +cd infoundry-mcp-server && npm install && npm run build && cd .. +``` + +### Run the UI +```bash +cd ui +npm run dev +# Open http://localhost:3000 +``` + +### Run the Oumi Server +```bash +cd oumi +python serve.py +# API available at http://localhost:8000 +``` + +## Project Structure + +``` +infoundry/ +├── docs/ # Documentation +├── examples/ # Sample data files +├── infoundry-mcp-server/ # Cline MCP integration +├── orchestrator/ # Kestra pipeline definitions +├── oumi/ # AI model training & serving +├── scripts/ # Utility scripts +├── tests/ # Unit tests +└── ui/ # Next.js React dashboard +``` + +## Documentation + +- [Blueprint & Architecture](docs/BLUEPRINT.md) - Full system design +- [Kestra Architecture](docs/kestra-architecture.md) - Pipeline documentation +- [Oumi Model](oumi/README.md) - AI model training guide +- [MCP Server](infoundry-mcp-server/README.md) - Cline integration + +## Testing + +```bash +# Python tests +pip install pytest fastapi httpx +pytest tests/ -v + +# MCP server tests +node tests/test_mcp_server.mjs +``` + +## License + +Released under the [MIT License](LICENSE). diff --git a/docs/architecture.md b/docs/architecture.md index bf24244..f2af086 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,9 +1,62 @@ -# Architecture Notes (MVP) +# InFoundry Architecture -- Orchestrator (FastAPI) exposes endpoints for summaries, recommendations, and PR triggers. -- Kestra pipelines ingest telemetry and produce `summary.json`. -- Oumi integration ranks actions from summaries. -- Cline integration renders IaC templates and opens PRs. -- CodeRabbit/GitHub Actions gate merges via terraform plan + tests. -- Simulation runner (localstack/kind) validates changes before merge; metrics feed back into rewards. +## System Overview +InFoundry is a self-adaptive Cloud Architect + SRE agent with the following components: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ User Interface │ +│ Next.js React Dashboard with React Flow Architecture Editor │ +└────────────────────────────┬────────────────────────────────────┘ + │ +┌────────────────────────────▼────────────────────────────────────┐ +│ Kestra Orchestrator │ +│ 9-step pipeline: Ingest → Propose → Generate → Deploy → Eval │ +└────────────────────────────┬────────────────────────────────────┘ + │ + ┌────────────────────┼────────────────────┐ + ▼ ▼ ▼ +┌───────────────┐ ┌───────────────┐ ┌───────────────┐ +│ Oumi Model │ │ MCP Server │ │ Terraform │ +│ Architecture │ │ Cline CLI │ │ Generator │ +│ Recommender │ │ Integration │ │ & Validator │ +└───────────────┘ └───────────────┘ └───────────────┘ +``` + +## Components + +### 1. UI Dashboard (`ui/`) +- Next.js 15 React application +- React Flow architecture diagram editor +- Real-time Kestra pipeline monitoring +- Service configuration generator + +### 2. Kestra Pipelines (`orchestrator/kestra_pipelines/`) +- `00-end-to-end.yaml` - Master orchestration pipeline +- Steps 01-09 for complete workflow + +### 3. Oumi Model (`oumi/`) +- Fine-tuned Qwen2.5-1.5B for architecture decisions +- FastAPI server with OpenAI-compatible API +- Training data generator + +### 4. MCP Server (`infoundry-mcp-server/`) +- Model Context Protocol server for Cline CLI +- Exposes all 9 workflow steps as tools +- TypeScript with Zod validation + +### 5. Scripts (`scripts/`) +- `generate-pr.sh` - Auto-generate PR descriptions + +## Data Flow + +1. **Ingest**: Analyze repository → Extract services, DBs, queues +2. **Telemetry**: Collect metrics → Summarize latency, errors, CPU +3. **Propose**: AI recommends → Pattern, components, scaling +4. **Render**: Generate graph → React Flow compatible JSON +5. **Generate**: Create Terraform → AWS infrastructure code +6. **Validate**: Run checks → fmt, validate, tflint, tfsec +7. **Create PR**: Open GitHub PR → With generated IaC +8. **Validate PR**: Check CI → Reviews, status checks +9. **Evaluate**: AI assessment → Recommendations for next iteration diff --git a/docs/runlocally.md b/docs/runlocally.md index 0795774..f004dcd 100644 --- a/docs/runlocally.md +++ b/docs/runlocally.md @@ -1,31 +1,65 @@ -# Run locally (scaffold) +# Run Locally -1) Repo analyzer -``` -python orchestrator/repo_analyzer/scan.py -``` +## Prerequisites +- Python 3.11+ +- Node.js 18+ +- Optional: Docker, kind, localstack -2) Telemetry summary (mocked) -``` -python orchestrator/kestra_pipelines/collect-and-summarize.yaml -``` -Use with Kestra or run the script section manually. +## Quick Start -3) Oumi stub -``` -python orchestrator/oumi_integration.py +### 1. UI Dashboard +```bash +cd ui +npm install +npm run dev +# Open http://localhost:3000 ``` -4) Generate IaC stub +### 2. Oumi Model Server +```bash +cd oumi + +# Setup virtual environment +python -m venv venv +source venv/bin/activate # or: source venv/bin/activate.fish + +# Install dependencies +pip install fastapi uvicorn + +# Run server +python serve.py +# API at http://localhost:8000 ``` -python -c "from orchestrator.cline_integration import propose_change; propose_change('auth')" + +### 3. MCP Server (for Cline integration) +```bash +cd infoundry-mcp-server +npm install +npm run build +npm start ``` -5) Simulation +### 4. Run Tests +```bash +# Python tests +pip install pytest fastapi httpx +pytest tests/ -v + +# MCP server integration test +node tests/test-all-tools.mjs ``` -python scripts/simulate_load.py -python scripts/compute_reward.py + +### 5. Utility Scripts +```bash +# Generate PR description +./scripts/generate-pr.sh dev ``` -These steps are placeholders; replace with real orchestrator endpoints and CI/CD when ready. +## Kestra Pipelines + +The Kestra pipelines are in `orchestrator/kestra_pipelines/`. To run them: +1. Install Kestra: https://kestra.io/docs/installation +2. Import the pipelines from `orchestrator/kestra_pipelines/` +3. Configure environment variables (GITHUB_TOKEN, etc.) +4. Trigger the `00-end-to-end.yaml` pipeline diff --git a/examples/telemetry/metrics.json b/examples/telemetry/metrics.json deleted file mode 100644 index 33d338b..0000000 --- a/examples/telemetry/metrics.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "auth": { - "latencies": [120, 140, 200, 450, 500, 320, 280, 260, 150, 130], - "errors": [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], - "cost": 60 - }, - "payments": { - "latencies": [90, 110, 130, 150, 170, 190, 210, 230, 250, 270], - "errors": [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], - "cost": 35 - } -} - diff --git a/infoundry-mcp-server/README.md b/infoundry-mcp-server/README.md index bb67402..c76a0b8 100644 --- a/infoundry-mcp-server/README.md +++ b/infoundry-mcp-server/README.md @@ -61,5 +61,6 @@ cline "Create a PR with the IaC files" ## Test ```bash -node test-all-tools.mjs +# From project root: +node tests/test_mcp_server.mjs ``` diff --git a/infoundry-mcp-server/package.json b/infoundry-mcp-server/package.json index b35b228..9b654ec 100644 --- a/infoundry-mcp-server/package.json +++ b/infoundry-mcp-server/package.json @@ -13,7 +13,7 @@ "dev": "tsx src/index.ts" }, "keywords": ["mcp", "cline", "terraform", "cloud-architecture"], - "license": "Apache-2.0", + "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", "zod": "^3.22.0" diff --git a/infoundry-mcp-server/test.mjs b/infoundry-mcp-server/test.mjs deleted file mode 100644 index 5cffe11..0000000 --- a/infoundry-mcp-server/test.mjs +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node -/** - * Test script for InFoundry MCP Server - * - * Usage: node test.mjs - */ - -import { spawn } from 'child_process'; -import * as readline from 'readline'; -import * as path from 'path'; -import { fileURLToPath } from 'url'; - -// Compute paths relative to this file for portability -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const projectRoot = path.resolve(__dirname, '..'); - -const server = spawn('node', ['dist/index.js'], { - cwd: __dirname, // Run from infoundry-mcp-server directory - stdio: ['pipe', 'pipe', 'pipe'] -}); - -let messageId = 1; - -function sendRequest(method, params = {}) { - const request = { - jsonrpc: '2.0', - id: messageId++, - method, - params - }; - console.log('\n→ Request:', JSON.stringify(request, null, 2)); - server.stdin.write(JSON.stringify(request) + '\n'); -} - -// Parse server output -const rl = readline.createInterface({ input: server.stdout }); -rl.on('line', (line) => { - try { - const response = JSON.parse(line); - console.log('\n← Response:', JSON.stringify(response, null, 2)); - } catch { - console.log('Server:', line); - } -}); - -server.stderr.on('data', (data) => { - console.log('Info:', data.toString().trim()); -}); - -// Wait for server to start, then run tests -setTimeout(() => { - console.log('\n=== Testing InFoundry MCP Server ===\n'); - - // 1. List available tools - sendRequest('tools/list'); - - // 2. Test analyze_repo after a delay - use project root for portability - setTimeout(() => { - sendRequest('tools/call', { - name: 'analyze_repo', - arguments: { - repoPath: projectRoot // Relative to this test file's location - } - }); - }, 500); - - // 3. Exit after tests - setTimeout(() => { - console.log('\n=== Tests complete ==='); - server.kill(); - process.exit(0); - }, 2000); - -}, 500); diff --git a/infra/.gitkeep b/infra/.gitkeep deleted file mode 100644 index 139597f..0000000 --- a/infra/.gitkeep +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/infra/modules/.gitkeep b/infra/modules/.gitkeep deleted file mode 100644 index 139597f..0000000 --- a/infra/modules/.gitkeep +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/infra/templates/.gitkeep b/infra/templates/.gitkeep deleted file mode 100644 index 139597f..0000000 --- a/infra/templates/.gitkeep +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/oumi/generate_training_data.py b/oumi/generate_training_data.py index f1170eb..341278a 100644 --- a/oumi/generate_training_data.py +++ b/oumi/generate_training_data.py @@ -12,6 +12,7 @@ import random import argparse +import json from pathlib import Path from typing import List, Dict, Any diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4cae1e8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +# InFoundry Python Dependencies + +# Core +fastapi>=0.100.0 +uvicorn>=0.23.0 +pydantic>=2.0.0 + +# Testing +pytest>=7.0.0 +httpx>=0.24.0 + +# Oumi (optional - for model training/serving) +# oumi[gpu] # Uncomment if training locally diff --git a/scripts/compute_reward.py b/scripts/compute_reward.py deleted file mode 100644 index de208af..0000000 --- a/scripts/compute_reward.py +++ /dev/null @@ -1,12 +0,0 @@ -def reward(before: dict, after: dict, weights=(0.4, 0.5, 0.1)) -> float: - cost_delta = (before.get("cost", 0) - after.get("cost", 0)) / max(1, before.get("cost", 1)) - latency_delta = (before.get("p95", 0) - after.get("p95", 0)) / max(1, before.get("p95", 1)) - reliability_delta = after.get("uptime", 1.0) - before.get("uptime", 1.0) - return weights[0] * cost_delta + weights[1] * latency_delta + weights[2] * reliability_delta - - -if __name__ == "__main__": - before_state = {"cost": 60, "p95": 420, "uptime": 0.99} - after_state = {"cost": 50, "p95": 310, "uptime": 0.995} - print(f"reward={reward(before_state, after_state):.4f}") - diff --git a/scripts/simulate_load.py b/scripts/simulate_load.py deleted file mode 100644 index e3a0f23..0000000 --- a/scripts/simulate_load.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -Minimal synthetic load script placeholder. -Extend with k6/vegeta integration or requests-based probes. -""" -import time -import random - - -def simulate(service: str, duration_seconds: int = 5) -> dict: - latencies = [random.randint(80, 250) for _ in range(50)] - errors = [0 if random.random() > 0.95 else 1 for _ in range(50)] - time.sleep(duration_seconds / 10) - return { - "service": service, - "p50": sorted(latencies)[25], - "p95": sorted(latencies)[47], - "error_rate": sum(errors) / len(errors), - } - - -if __name__ == "__main__": - print(simulate("auth")) - diff --git a/services/auth/Dockerfile b/services/auth/Dockerfile deleted file mode 100644 index 7a6fd12..0000000 --- a/services/auth/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.11-slim -WORKDIR /app -COPY app.py /app/app.py -RUN pip install fastapi uvicorn -EXPOSE 8000 -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] - diff --git a/services/auth/app.py b/services/auth/app.py deleted file mode 100644 index 64835a2..0000000 --- a/services/auth/app.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/healthz") -async def health(): - return {"status": "ok"} - - -@app.get("/hello") -async def hello(): - return {"service": "auth", "message": "hello from auth"} - - diff --git a/services/payments/Dockerfile b/services/payments/Dockerfile deleted file mode 100644 index 9fd4e50..0000000 --- a/services/payments/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.11-slim -WORKDIR /app -COPY app.py /app/app.py -RUN pip install fastapi uvicorn -EXPOSE 8001 -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8001"] - diff --git a/services/payments/app.py b/services/payments/app.py deleted file mode 100644 index 86a13cf..0000000 --- a/services/payments/app.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/healthz") -async def health(): - return {"status": "ok"} - - -@app.get("/charge") -async def charge(): - return {"service": "payments", "message": "mock charge processed"} - - diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index 139597f..0000000 --- a/tests/.gitkeep +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/tests/test_generate_training_data.py b/tests/test_generate_training_data.py new file mode 100644 index 0000000..147600f --- /dev/null +++ b/tests/test_generate_training_data.py @@ -0,0 +1,387 @@ +""" +Unit tests for oumi/generate_training_data.py + +Tests the training data generation functions including pattern determination, +component generation, rationale generation, and example generation. +""" + +import sys +import os +import json +import pytest + +# Add oumi directory to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'oumi')) + +from generate_training_data import ( + determine_pattern, + generate_components, + generate_rationale, + generate_example, + PATTERNS, + DATABASES, + SERVICE_TYPES, +) + + +class TestDeterminePattern: + """Tests for the determine_pattern() function.""" + + def test_gpu_returns_kubernetes(self): + """Test GPU workloads return kubernetes pattern.""" + result = determine_pattern( + services=["api"], + db="postgres", + app_type="web", + gpu=True + ) + assert result == "kubernetes" + + def test_kafka_returns_event_driven(self): + """Test Kafka queue returns event_driven pattern.""" + result = determine_pattern( + services=["api", "worker"], + db="postgres", + app_type="web", + queue="kafka" + ) + assert result == "event_driven" + + def test_scheduled_single_service_serverless(self): + """Test scheduled single service returns serverless.""" + result = determine_pattern( + services=["cron-job"], + db="dynamodb", + app_type="scheduled", + scheduled=True + ) + assert result == "serverless" + + def test_single_service_dynamodb_serverless(self): + """Test single service with DynamoDB returns serverless.""" + result = determine_pattern( + services=["api"], + db="dynamodb", + app_type="web" + ) + assert result == "serverless" + + def test_five_services_kubernetes(self): + """Test 5+ services returns kubernetes.""" + result = determine_pattern( + services=["api", "web", "auth", "payments", "notifications"], + db="postgres", + app_type="web" + ) + assert result == "kubernetes" + + def test_six_services_kubernetes(self): + """Test 6 services returns kubernetes.""" + result = determine_pattern( + services=["api", "web", "auth", "payments", "notifications", "analytics"], + db="postgres", + app_type="web" + ) + assert result == "kubernetes" + + def test_two_services_microservices_ecs(self): + """Test 2 services returns microservices_ecs.""" + result = determine_pattern( + services=["api", "web"], + db="postgres", + app_type="web" + ) + assert result == "microservices_ecs" + + def test_three_services_microservices_ecs(self): + """Test 3 services returns microservices_ecs.""" + result = determine_pattern( + services=["api", "web", "worker"], + db="postgres", + app_type="web" + ) + assert result == "microservices_ecs" + + def test_four_services_microservices_ecs(self): + """Test 4 services returns microservices_ecs.""" + result = determine_pattern( + services=["api", "web", "worker", "auth"], + db="postgres", + app_type="web" + ) + assert result == "microservices_ecs" + + def test_single_legacy_lift_and_shift(self): + """Test single legacy service returns lift_and_shift.""" + result = determine_pattern( + services=["monolith"], + db="postgres", + app_type="legacy" + ) + assert result == "lift_and_shift" + + def test_single_enterprise_lift_and_shift(self): + """Test single enterprise service returns lift_and_shift.""" + result = determine_pattern( + services=["erp"], + db="postgres", + app_type="enterprise" + ) + assert result == "lift_and_shift" + + def test_single_service_default_serverless(self): + """Test single non-specific service defaults to serverless.""" + result = determine_pattern( + services=["api"], + db="postgres", + app_type="web" + ) + assert result == "serverless" + + +class TestGenerateComponents: + """Tests for the generate_components() function.""" + + def test_returns_list(self): + """Test that function returns a list.""" + result = generate_components("serverless", "dynamodb") + assert isinstance(result, list) + + def test_includes_base_pattern_components(self): + """Test that base pattern components are included.""" + result = generate_components("serverless", "dynamodb") + + # Serverless should have at least some base components + base_components = PATTERNS["serverless"]["components"] + for comp in base_components[:3]: # Check first few + # At least some should be present + pass # Components are included in base + + def test_adds_database_component(self): + """Test that appropriate database component is added.""" + result = generate_components("microservices_ecs", "postgres") + assert "rds" in result + + def test_dynamodb_mapping(self): + """Test DynamoDB component mapping.""" + result = generate_components("serverless", "dynamodb") + assert "dynamodb" in result + + def test_mongodb_mapping(self): + """Test MongoDB to DocumentDB mapping.""" + result = generate_components("microservices_ecs", "mongodb") + assert "documentdb" in result + + def test_redis_database_mapping(self): + """Test Redis to ElastiCache mapping.""" + result = generate_components("microservices_ecs", "redis") + assert "elasticache" in result + + def test_adds_queue_component(self): + """Test that queue component is added.""" + result = generate_components("event_driven", "postgres", queue="sqs") + assert "sqs" in result + + def test_kafka_queue_mapping(self): + """Test Kafka to MSK mapping.""" + result = generate_components("event_driven", "postgres", queue="kafka") + assert "msk" in result + + def test_rabbitmq_queue_mapping(self): + """Test RabbitMQ to Amazon MQ mapping.""" + result = generate_components("microservices_ecs", "postgres", queue="rabbitmq") + assert "amazon_mq" in result + + def test_no_duplicates(self): + """Test that returned components have no duplicates.""" + result = generate_components("kubernetes", "postgres", queue="redis") + assert len(result) == len(set(result)) + + def test_all_patterns_work(self): + """Test that all patterns can generate components.""" + for pattern in PATTERNS.keys(): + result = generate_components(pattern, "postgres") + assert isinstance(result, list) + assert len(result) > 0 + + +class TestGenerateRationale: + """Tests for the generate_rationale() function.""" + + def test_returns_string(self): + """Test that function returns a string.""" + result = generate_rationale( + pattern="serverless", + services=["api"], + db="dynamodb" + ) + assert isinstance(result, str) + + def test_string_not_empty(self): + """Test that rationale is not empty.""" + result = generate_rationale( + pattern="serverless", + services=["api"], + db="dynamodb" + ) + assert len(result) > 0 + + def test_all_patterns_have_rationales(self): + """Test that all patterns can generate rationales.""" + patterns = ["serverless", "microservices_ecs", "kubernetes", "event_driven", "lift_and_shift"] + + for pattern in patterns: + result = generate_rationale( + pattern=pattern, + services=["api"], + db="postgres" + ) + assert isinstance(result, str) + assert len(result) > 0 + + def test_unknown_pattern_has_fallback(self): + """Test that unknown patterns have a fallback rationale.""" + result = generate_rationale( + pattern="unknown_pattern", + services=["api"], + db="postgres" + ) + assert isinstance(result, str) + assert len(result) > 0 + + +class TestGenerateExample: + """Tests for the generate_example() function.""" + + def test_returns_dict(self): + """Test that function returns a dictionary.""" + result = generate_example() + assert isinstance(result, dict) + + def test_has_messages_key(self): + """Test that result has messages key.""" + result = generate_example() + assert "messages" in result + + def test_messages_is_list(self): + """Test that messages is a list.""" + result = generate_example() + assert isinstance(result["messages"], list) + + def test_has_three_messages(self): + """Test that there are exactly 3 messages (system, user, assistant).""" + result = generate_example() + assert len(result["messages"]) == 3 + + def test_message_roles_correct(self): + """Test that message roles are system, user, assistant.""" + result = generate_example() + messages = result["messages"] + + assert messages[0]["role"] == "system" + assert messages[1]["role"] == "user" + assert messages[2]["role"] == "assistant" + + def test_all_messages_have_content(self): + """Test that all messages have content.""" + result = generate_example() + + for message in result["messages"]: + assert "role" in message + assert "content" in message + assert len(message["content"]) > 0 + + def test_assistant_content_is_valid_json(self): + """Test that assistant content is valid JSON.""" + result = generate_example() + assistant_content = result["messages"][2]["content"] + + parsed = json.loads(assistant_content) + assert isinstance(parsed, dict) + + def test_assistant_json_has_architecture(self): + """Test that assistant JSON has architecture key.""" + result = generate_example() + assistant_content = result["messages"][2]["content"] + parsed = json.loads(assistant_content) + + assert "architecture" in parsed + + def test_architecture_has_required_keys(self): + """Test that architecture has required keys.""" + result = generate_example() + assistant_content = result["messages"][2]["content"] + parsed = json.loads(assistant_content) + + architecture = parsed["architecture"] + assert "pattern" in architecture + assert "components" in architecture + assert "rationale" in architecture + + def test_has_inputs_key(self): + """Test that assistant JSON has inputs key.""" + result = generate_example() + assistant_content = result["messages"][2]["content"] + parsed = json.loads(assistant_content) + + assert "inputs" in parsed + assert "service_count" in parsed["inputs"] + assert "cloud_provider" in parsed["inputs"] + + def test_has_source_key(self): + """Test that assistant JSON has source key.""" + result = generate_example() + assistant_content = result["messages"][2]["content"] + parsed = json.loads(assistant_content) + + assert "source" in parsed + assert parsed["source"] == "ai_recommendation" + + def test_pattern_is_valid(self): + """Test that pattern is one of the valid patterns.""" + valid_patterns = list(PATTERNS.keys()) + + for _ in range(10): # Run multiple times due to randomness + result = generate_example() + assistant_content = result["messages"][2]["content"] + parsed = json.loads(assistant_content) + + pattern = parsed["architecture"]["pattern"] + assert pattern in valid_patterns + + def test_components_are_strings(self): + """Test that components is a list of strings.""" + result = generate_example() + assistant_content = result["messages"][2]["content"] + parsed = json.loads(assistant_content) + + components = parsed["architecture"]["components"] + assert isinstance(components, list) + + for comp in components: + assert isinstance(comp, str) + + def test_user_content_contains_services(self): + """Test that user content contains 'Services:' info.""" + result = generate_example() + user_content = result["messages"][1]["content"] + + assert "Services:" in user_content + + def test_user_content_contains_db(self): + """Test that user content contains 'DB:' info.""" + result = generate_example() + user_content = result["messages"][1]["content"] + + assert "DB:" in user_content + + def test_user_content_contains_cloud(self): + """Test that user content contains 'Cloud:' info.""" + result = generate_example() + user_content = result["messages"][1]["content"] + + assert "Cloud:" in user_content + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_kestra.js b/tests/test_kestra.js new file mode 100644 index 0000000..3588876 --- /dev/null +++ b/tests/test_kestra.js @@ -0,0 +1,260 @@ +/** + * Unit tests for ui/lib/kestra.js + * + * Tests the Kestra API utility functions including state mapping, + * step progress mapping, and duration calculation. + * + * Run with: node test_kestra.js + */ + +import assert from 'node:assert'; +import { describe, it } from 'node:test'; + +// Import the functions to test +import { + PIPELINE_STEPS, + STATE_MAP, + mapToStepProgress, + calculateDuration, +} from '../ui/lib/kestra.js'; + + +describe('PIPELINE_STEPS', () => { + it('should have 9 pipeline steps', () => { + assert.strictEqual(PIPELINE_STEPS.length, 9); + }); + + it('should have correct step IDs', () => { + const expectedIds = [ + 'ingest_repo', + 'ingest_telemetry', + 'propose_architecture', + 'render_graph', + 'generate_iac', + 'validate_iac', + 'create_pr', + 'validate_pr', + 'evaluate', + ]; + + const actualIds = PIPELINE_STEPS.map(step => step.id); + assert.deepStrictEqual(actualIds, expectedIds); + }); + + it('should have sequential order values', () => { + for (let i = 0; i < PIPELINE_STEPS.length; i++) { + assert.strictEqual(PIPELINE_STEPS[i].order, i + 1); + } + }); + + it('should have label for each step', () => { + for (const step of PIPELINE_STEPS) { + assert.ok(step.label, `Step ${step.id} should have a label`); + assert.ok(typeof step.label === 'string', `Step ${step.id} label should be string`); + } + }); +}); + + +describe('STATE_MAP', () => { + it('should map CREATED to pending', () => { + assert.strictEqual(STATE_MAP.CREATED, 'pending'); + }); + + it('should map QUEUED to pending', () => { + assert.strictEqual(STATE_MAP.QUEUED, 'pending'); + }); + + it('should map RUNNING to running', () => { + assert.strictEqual(STATE_MAP.RUNNING, 'running'); + }); + + it('should map SUCCESS to completed', () => { + assert.strictEqual(STATE_MAP.SUCCESS, 'completed'); + }); + + it('should map WARNING to completed', () => { + assert.strictEqual(STATE_MAP.WARNING, 'completed'); + }); + + it('should map FAILED to failed', () => { + assert.strictEqual(STATE_MAP.FAILED, 'failed'); + }); + + it('should map RETRYING to running', () => { + assert.strictEqual(STATE_MAP.RETRYING, 'running'); + }); + + it('should map PAUSED to pending', () => { + assert.strictEqual(STATE_MAP.PAUSED, 'pending'); + }); + + it('should map KILLED to failed', () => { + assert.strictEqual(STATE_MAP.KILLED, 'failed'); + }); + + it('should have all expected states', () => { + const expectedStates = ['CREATED', 'QUEUED', 'RUNNING', 'SUCCESS', 'WARNING', 'FAILED', 'RETRYING', 'PAUSED', 'KILLED']; + for (const state of expectedStates) { + assert.ok(state in STATE_MAP, `STATE_MAP should include ${state}`); + } + }); +}); + + +describe('mapToStepProgress', () => { + it('should return array with same length as PIPELINE_STEPS', () => { + const executionData = { taskRuns: [] }; + const result = mapToStepProgress(executionData); + + assert.strictEqual(result.length, PIPELINE_STEPS.length); + }); + + it('should return pending state for missing tasks', () => { + const executionData = { taskRuns: [] }; + const result = mapToStepProgress(executionData); + + for (const step of result) { + assert.strictEqual(step.state, 'pending'); + } + }); + + it('should return empty outputs for missing tasks', () => { + const executionData = { taskRuns: [] }; + const result = mapToStepProgress(executionData); + + for (const step of result) { + assert.deepStrictEqual(step.outputs, {}); + } + }); + + it('should map task run states correctly', () => { + const executionData = { + taskRuns: [ + { id: 'ingest_repo', state: 'SUCCESS', outputs: { result: 'done' } } + ] + }; + + const result = mapToStepProgress(executionData); + const ingestStep = result.find(s => s.id === 'ingest_repo'); + + assert.strictEqual(ingestStep.state, 'SUCCESS'); + assert.deepStrictEqual(ingestStep.outputs, { result: 'done' }); + }); + + it('should include startDate and endDate from task runs', () => { + const startDate = '2024-01-01T10:00:00Z'; + const endDate = '2024-01-01T10:01:00Z'; + + const executionData = { + taskRuns: [ + { id: 'ingest_repo', state: 'SUCCESS', startDate, endDate, outputs: {} } + ] + }; + + const result = mapToStepProgress(executionData); + const ingestStep = result.find(s => s.id === 'ingest_repo'); + + assert.strictEqual(ingestStep.startDate, startDate); + assert.strictEqual(ingestStep.endDate, endDate); + }); + + it('should preserve original step properties', () => { + const executionData = { taskRuns: [] }; + const result = mapToStepProgress(executionData); + + for (let i = 0; i < result.length; i++) { + assert.strictEqual(result[i].id, PIPELINE_STEPS[i].id); + assert.strictEqual(result[i].label, PIPELINE_STEPS[i].label); + assert.strictEqual(result[i].order, PIPELINE_STEPS[i].order); + } + }); + + it('should handle multiple task runs', () => { + const executionData = { + taskRuns: [ + { id: 'ingest_repo', state: 'SUCCESS', outputs: {} }, + { id: 'ingest_telemetry', state: 'RUNNING', outputs: {} }, + { id: 'propose_architecture', state: 'QUEUED', outputs: {} }, + ] + }; + + const result = mapToStepProgress(executionData); + + assert.strictEqual(result.find(s => s.id === 'ingest_repo').state, 'SUCCESS'); + assert.strictEqual(result.find(s => s.id === 'ingest_telemetry').state, 'RUNNING'); + assert.strictEqual(result.find(s => s.id === 'propose_architecture').state, 'QUEUED'); + assert.strictEqual(result.find(s => s.id === 'render_graph').state, 'pending'); + }); +}); + + +describe('calculateDuration', () => { + it('should return null if startDate is missing', () => { + const result = calculateDuration(null, '2024-01-01T10:01:00Z'); + assert.strictEqual(result, null); + }); + + it('should return null if endDate is missing', () => { + const result = calculateDuration('2024-01-01T10:00:00Z', null); + assert.strictEqual(result, null); + }); + + it('should return null if both dates are missing', () => { + const result = calculateDuration(null, null); + assert.strictEqual(result, null); + }); + + it('should return milliseconds for sub-second durations', () => { + const startDate = '2024-01-01T10:00:00.000Z'; + const endDate = '2024-01-01T10:00:00.500Z'; + + const result = calculateDuration(startDate, endDate); + assert.strictEqual(result, '500ms'); + }); + + it('should return seconds for sub-minute durations', () => { + const startDate = '2024-01-01T10:00:00Z'; + const endDate = '2024-01-01T10:00:30Z'; + + const result = calculateDuration(startDate, endDate); + assert.strictEqual(result, '30s'); + }); + + it('should return minutes for sub-hour durations', () => { + const startDate = '2024-01-01T10:00:00Z'; + const endDate = '2024-01-01T10:05:00Z'; + + const result = calculateDuration(startDate, endDate); + assert.strictEqual(result, '5m'); + }); + + it('should return hours for long durations', () => { + const startDate = '2024-01-01T10:00:00Z'; + const endDate = '2024-01-01T12:00:00Z'; + + const result = calculateDuration(startDate, endDate); + assert.strictEqual(result, '2h'); + }); + + it('should round seconds correctly', () => { + const startDate = '2024-01-01T10:00:00Z'; + const endDate = '2024-01-01T10:00:45Z'; + + const result = calculateDuration(startDate, endDate); + assert.strictEqual(result, '45s'); + }); + + it('should round minutes correctly', () => { + const startDate = '2024-01-01T10:00:00Z'; + const endDate = '2024-01-01T10:30:00Z'; + + const result = calculateDuration(startDate, endDate); + assert.strictEqual(result, '30m'); + }); +}); + + +// Run tests summary +console.log('\n✅ All Kestra utility tests defined'); +console.log('Run with: node --test tests/test_kestra.js\n'); diff --git a/infoundry-mcp-server/test-all-tools.mjs b/tests/test_mcp_server.mjs similarity index 95% rename from infoundry-mcp-server/test-all-tools.mjs rename to tests/test_mcp_server.mjs index fdbc7d3..f9abcca 100644 --- a/infoundry-mcp-server/test-all-tools.mjs +++ b/tests/test_mcp_server.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env node /** - * Test script for InFoundry MCP Server - All 9 Workflow Steps + * MCP Server Integration Test + * Tests all 9 InFoundry workflow tools via the MCP protocol */ import { spawn } from 'child_process'; @@ -11,9 +12,10 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const projectRoot = path.resolve(__dirname, '..'); +const mcpServerDir = path.join(projectRoot, 'infoundry-mcp-server'); const server = spawn('node', ['dist/index.js'], { - cwd: __dirname, + cwd: mcpServerDir, stdio: ['pipe', 'pipe', 'pipe'] }); diff --git a/tests/test_oumi_serve.py b/tests/test_oumi_serve.py new file mode 100644 index 0000000..c040697 --- /dev/null +++ b/tests/test_oumi_serve.py @@ -0,0 +1,292 @@ +""" +Unit tests for oumi/serve.py + +Tests the FastAPI server endpoints and heuristic response function. +Uses FastAPI's TestClient for endpoint testing. +""" + +import sys +import os +import json +import pytest + +# Add oumi directory to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'oumi')) + +# Import the FastAPI app and functions +from serve import app, heuristic_response + +# Import TestClient for API testing +from fastapi.testclient import TestClient + +client = TestClient(app) + + +class TestHeuristicResponse: + """Tests for the heuristic_response() function.""" + + def test_returns_dict_with_required_keys(self): + """Test that function returns dict with pattern, components, rationale.""" + result = heuristic_response("test prompt") + + assert isinstance(result, dict) + assert "pattern" in result + assert "components" in result + assert "rationale" in result + + def test_kafka_pattern_detection(self): + """Test that kafka triggers event_driven pattern.""" + result = heuristic_response("my app uses kafka") + + assert result["pattern"] == "event_driven" + assert "msk" in result["components"] + + def test_queue_pattern_detection(self): + """Test that queue keyword triggers event_driven pattern.""" + result = heuristic_response("need a message queue system") + + assert result["pattern"] == "event_driven" + + def test_kubernetes_pattern_detection(self): + """Test that kubernetes keyword triggers kubernetes pattern.""" + result = heuristic_response("deploy on kubernetes cluster") + + assert result["pattern"] == "kubernetes" + assert "eks_cluster" in result["components"] + + def test_five_services_kubernetes(self): + """Test that 5+ services triggers kubernetes pattern.""" + result = heuristic_response("we have 5 microservices") + + assert result["pattern"] == "kubernetes" + + def test_six_services_kubernetes(self): + """Test that 6+ services triggers kubernetes pattern.""" + result = heuristic_response("we have 6 microservices") + + assert result["pattern"] == "kubernetes" + + def test_serverless_pattern_detection(self): + """Test that serverless keyword triggers serverless pattern.""" + result = heuristic_response("want a serverless backend") + + assert result["pattern"] == "serverless" + assert "api_gateway" in result["components"] + assert "lambda_functions" in result["components"] + + def test_lambda_pattern_detection(self): + """Test that lambda keyword triggers serverless pattern.""" + result = heuristic_response("deploy lambda functions") + + assert result["pattern"] == "serverless" + + def test_default_microservices_ecs(self): + """Test default pattern when no specific keywords match.""" + result = heuristic_response("build a simple web app") + + assert result["pattern"] == "microservices_ecs" + assert "ecs_cluster" in result["components"] + + def test_high_latency_adds_components(self): + """Test that 'high' keyword adds extra components.""" + result = heuristic_response("need high performance") + + # Should add elasticache, cdn, or hpa + extended_components = ["elasticache", "cdn", "hpa"] + has_extended = any(c in result["components"] for c in extended_components) + assert has_extended + + def test_latency_keyword_adds_components(self): + """Test that 'latency' keyword adds extra components.""" + result = heuristic_response("low latency is important") + + extended_components = ["elasticache", "cdn", "hpa"] + has_extended = any(c in result["components"] for c in extended_components) + assert has_extended + + def test_components_are_unique(self): + """Test that components list has no duplicates.""" + result = heuristic_response("high latency kubernetes cluster") + + components = result["components"] + assert len(components) == len(set(components)) + + def test_rationale_contains_pattern(self): + """Test that rationale mentions the selected pattern.""" + result = heuristic_response("test prompt") + + assert result["pattern"] in result["rationale"] + + +class TestHealthEndpoint: + """Tests for the /health endpoint.""" + + def test_health_returns_200(self): + """Test that health endpoint returns HTTP 200.""" + response = client.get("/health") + + assert response.status_code == 200 + + def test_health_response_structure(self): + """Test that health response has required keys.""" + response = client.get("/health") + data = response.json() + + assert "status" in data + assert "ollama_available" in data + assert "ollama_url" in data + + def test_health_status_is_healthy(self): + """Test that status is 'healthy'.""" + response = client.get("/health") + data = response.json() + + assert data["status"] == "healthy" + + def test_ollama_available_is_boolean(self): + """Test that ollama_available is a boolean.""" + response = client.get("/health") + data = response.json() + + assert isinstance(data["ollama_available"], bool) + + +class TestChatCompletionsEndpoint: + """Tests for the /v1/chat/completions endpoint.""" + + def test_returns_200_with_messages(self): + """Test endpoint returns 200 with valid messages input.""" + response = client.post( + "/v1/chat/completions", + json={ + "model": "test", + "messages": [{"role": "user", "content": "build a simple api"}] + } + ) + + assert response.status_code == 200 + + def test_returns_200_with_prompt(self): + """Test endpoint returns 200 with prompt input.""" + response = client.post( + "/v1/chat/completions", + json={ + "model": "test", + "prompt": "build a simple api" + } + ) + + assert response.status_code == 200 + + def test_returns_400_without_prompt(self): + """Test endpoint returns 400 when no prompt provided.""" + response = client.post( + "/v1/chat/completions", + json={"model": "test"} + ) + + assert response.status_code == 400 + + def test_response_has_correct_id(self): + """Test response has cmpl-infoundry id.""" + response = client.post( + "/v1/chat/completions", + json={"prompt": "test"} + ) + data = response.json() + + assert data["id"] == "cmpl-infoundry" + + def test_response_has_choices(self): + """Test response has choices array with message.""" + response = client.post( + "/v1/chat/completions", + json={"prompt": "test"} + ) + data = response.json() + + assert "choices" in data + assert len(data["choices"]) > 0 + assert "message" in data["choices"][0] + + def test_message_role_is_assistant(self): + """Test message role is assistant.""" + response = client.post( + "/v1/chat/completions", + json={"prompt": "test"} + ) + data = response.json() + + message = data["choices"][0]["message"] + assert message["role"] == "assistant" + + def test_finish_reason_is_stop(self): + """Test finish_reason is stop.""" + response = client.post( + "/v1/chat/completions", + json={"prompt": "test"} + ) + data = response.json() + + assert data["choices"][0]["finish_reason"] == "stop" + + def test_content_is_valid_json(self): + """Test that message content is valid JSON (heuristic fallback).""" + response = client.post( + "/v1/chat/completions", + json={"prompt": "simple web service"} + ) + data = response.json() + + content = data["choices"][0]["message"]["content"] + # Should be able to parse as JSON + parsed = json.loads(content) + assert "pattern" in parsed + + +class TestCompletionsEndpoint: + """Tests for the /v1/completions endpoint.""" + + def test_returns_200(self): + """Test endpoint returns 200 with prompt.""" + response = client.post( + "/v1/completions", + json={"prompt": "build an api"} + ) + + assert response.status_code == 200 + + def test_response_has_choices(self): + """Test response has choices array.""" + response = client.post( + "/v1/completions", + json={"prompt": "build an api"} + ) + data = response.json() + + assert "choices" in data + assert len(data["choices"]) > 0 + + def test_choice_has_text(self): + """Test that choice has text field.""" + response = client.post( + "/v1/completions", + json={"prompt": "build an api"} + ) + data = response.json() + + assert "text" in data["choices"][0] + + def test_finish_reason_is_stop(self): + """Test finish_reason is stop.""" + response = client.post( + "/v1/completions", + json={"prompt": "test"} + ) + data = response.json() + + assert data["choices"][0]["finish_reason"] == "stop" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/ui/README.md b/ui/README.md index daba75d..8fae5c9 100644 --- a/ui/README.md +++ b/ui/README.md @@ -1,51 +1,49 @@ -# CloudGenesis UI +# InFoundry UI -Next.js 14 app scaffold for the CloudGenesis dashboard (Project Overview, Recommendations, PR Monitor, Before/After charts). +Next.js 15 dashboard for InFoundry cloud architecture platform. -## Develop -``` -cd ui -npm install -npm run dev -``` - -## Notes -- Strict TypeScript enabled. -- Styling uses simple CSS modules; feel free to swap to Tailwind/Chakra. -- Wire backend calls to the orchestrator for summaries, recommendations, and PR status. -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started +## Features +- 🏗️ Visual Architecture Editor (React Flow) +- 📊 Kestra Pipeline Monitoring +- ⚙️ Service Configuration Generator +- 📈 Real-time Step Progress Tracking -First, run the development server: +## Development ```bash +cd ui +npm install npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Open [http://localhost:3000](http://localhost:3000) to view the dashboard. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +## Pages -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +| Route | Description | +|-------|-------------| +| `/` | Landing page with feature overview | +| `/dashboard` | Architecture diagram editor | +| `/pipeline` | Kestra pipeline runner & monitor | +| `/configure` | Service profile configuration | -## Learn More +## Environment Variables -To learn more about Next.js, take a look at the following resources: +Create `.env.local`: -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +```bash +KESTRA_API_URL=http://localhost:8080 +``` -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +## Build + +```bash +npm run build +npm start +``` ## Deploy on Vercel -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new?utm_medium=default-template&filter=next.js) -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +Check [Next.js deployment docs](https://nextjs.org/docs/app/building-your-application/deploying) for details.