diff --git a/.bazelrc b/.bazelrc index 1c356c0..0d2b298 100644 --- a/.bazelrc +++ b/.bazelrc @@ -17,7 +17,6 @@ build --announce_rc # Show test failures immediately. test --test_output=errors -test --test_verbose_timeout_warnings test --action_env=PATH test --action_env=HOME @@ -26,4 +25,3 @@ coverage --combined_report=lcov # User-local overrides (output_base, remote cache, etc.). try-import %workspace%/.bazelrc.project -export PATH="/Users/gabrielfonseca/.cargo/bin:/Users/gabrielfonseca/.bun/bin:/Users/gabrielfonseca/.grok/bin:/Users/gabrielfonseca/.codeium/windsurf/bin:/Users/gabrielfonseca/bin:/Users/gabrielfonseca/.antigravity/antigravity/bin:/Users/gabrielfonseca/.antigravity/antigravity/bin:/Users/gabrielfonseca/.opencode/bin:/Users/gabrielfonseca/.antigravity/antigravity/bin:/Users/gabrielfonseca/.antigravity/antigravity/bin:/Users/gabrielfonseca/.local/bin:/Users/gabrielfonseca/.bun/bin:/Users/gabrielfonseca/.nvm/versions/node/v22.22.3/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/Library/Apple/usr/bin:/opt/homebrew/bin:/Users/gabrielfonseca/.grok/bin:/Users/gabrielfonseca/.codeium/windsurf/bin:/Users/gabrielfonseca/bin:/Users/gabrielfonseca/.antigravity/antigravity/bin:/Users/gabrielfonseca/.opencode/bin:/Users/gabrielfonseca/.local/bin:/Users/gabrielfonseca/.bun/bin:/Users/gabrielfonseca/.nvm/versions/node/v22.22.3/bin:/opt/homebrew/sbin:/Users/gabrielfonseca/Library/pnpm:/Users/gabrielfonseca/.cargo/bin:/Users/gabrielfonseca/.turso:/Users/gabrielfonseca/.turso" diff --git a/.env.example b/.env.example index 7de8ea5..d03fb65 100644 --- a/.env.example +++ b/.env.example @@ -40,26 +40,26 @@ OLLAMA_MODEL=llama2 # CONFIGURATION DIRECTORIES # =========================================== -FCODE_CONFIG_HOME=~/.frontal-code -FCODE_HOME=~/.frontal-code +FRONTAL_CODE_CONFIG_HOME=~/.frontal-code +FRONTAL_CODE_HOME=~/.frontal-code HOME=~/ # =========================================== # DOCKER & SERVICE CONFIGURATION # =========================================== -FCODE_MEMORY_PINECONE_URL=https://YOUR_INDEX_HOST -FCODE_MEMORY_PINECONE_API_KEY=your_pinecone_api_key_here -FCODE_MEMORY_PINECONE_NAMESPACE=dev +FRONTAL_CODE_MEMORY_PINECONE_URL=https://YOUR_INDEX_HOST +FRONTAL_CODE_MEMORY_PINECONE_API_KEY=your_pinecone_api_key_here +FRONTAL_CODE_MEMORY_PINECONE_NAMESPACE=dev # Docker-specific paths -FCODE_SANDBOX_HOME=/workspace/.sandbox-home +FRONTAL_CODE_SANDBOX_HOME=/workspace/.sandbox-home # Sandbox backend controls # Enable Docker fallback backend explicitly (default: disabled) -FCODE_SANDBOX_ENABLE_DOCKER=0 -# Optional Docker image override when FCODE_SANDBOX_ENABLE_DOCKER=1 -FCODE_SANDBOX_DOCKER_IMAGE=busybox:1.36 +FRONTAL_CODE_SANDBOX_ENABLE_DOCKER=0 +# Optional Docker image override when FRONTAL_CODE_SANDBOX_ENABLE_DOCKER=1 +FRONTAL_CODE_SANDBOX_DOCKER_IMAGE=busybox:1.36 # Service Configuration @@ -85,17 +85,17 @@ MOCK_PARITY_REPORT_PATH=/tmp/mock_parity_report.json # --- frontal-code-server control plane ------------------------------------------- # Required. Without it the server refuses to start, because every task route # (create/cancel/complete/approval) would accept unauthenticated requests. -FCODE_SERVER_API_KEY=change_me +FRONTAL_CODE_SERVER_API_KEY=change_me # Set to 1 only to deliberately run an open control plane on a trusted network. -# FCODE_SERVER_ALLOW_ANONYMOUS=1 +# FRONTAL_CODE_SERVER_ALLOW_ANONYMOUS=1 # --- Integration webhooks --------------------------------------------------- -# One secret per source, named FCODE__WEBHOOK_SECRET. A delivery to +# One secret per source, named FRONTAL_CODE__WEBHOOK_SECRET. A delivery to # /v1/webhooks/ is rejected with 401 unless the matching secret is set # and the request carries a valid `x--signature` HMAC. -FCODE_GITHUB_WEBHOOK_SECRET=change_me -# FCODE_LINEAR_WEBHOOK_SECRET=change_me +FRONTAL_CODE_GITHUB_WEBHOOK_SECRET=change_me +# FRONTAL_CODE_LINEAR_WEBHOOK_SECRET=change_me # =========================================== # TOOLS.FRONTAL.DEV CONFIGURATION @@ -104,10 +104,10 @@ FCODE_GITHUB_WEBHOOK_SECRET=change_me # Frontal Code CLI configuration for tools.frontal.dev # Set these when using deployed infrastructure: # export FRONTAL_BASE_URL="https://tools.frontal.dev/frontal-code" -# export FCODE_HOSTED_CALLBACK_URL="https://tools.frontal.dev/frontal-code/webhooks/tasks" +# export FRONTAL_CODE_HOSTED_CALLBACK_URL="https://tools.frontal.dev/frontal-code/webhooks/tasks" # Hosted task worker configuration -FCODE_HOSTED_TASK_FILE=/tmp/frontal-code_hosted_task.json +FRONTAL_CODE_HOSTED_TASK_FILE=/tmp/frontal-code_hosted_task.json # =========================================== # OPTIONAL OVERRIDES diff --git a/.frontal-code/settings.json b/.frontal-code/settings.json index e1f4448..e36dc6a 100644 --- a/.frontal-code/settings.json +++ b/.frontal-code/settings.json @@ -120,8 +120,8 @@ "read:org", "workflow" ], - "client_id_env": "FCODE_GITHUB_CLIENT_ID", - "client_secret_env": "FCODE_GITHUB_CLIENT_SECRET" + "client_id_env": "FRONTAL_CODE_GITHUB_CLIENT_ID", + "client_secret_env": "FRONTAL_CODE_GITHUB_CLIENT_SECRET" } }, "linear": { @@ -138,8 +138,8 @@ "issues:create", "comments:create" ], - "client_id_env": "FCODE_LINEAR_CLIENT_ID", - "client_secret_env": "FCODE_LINEAR_CLIENT_SECRET" + "client_id_env": "FRONTAL_CODE_LINEAR_CLIENT_ID", + "client_secret_env": "FRONTAL_CODE_LINEAR_CLIENT_SECRET" } }, "graphite": { @@ -154,8 +154,8 @@ "stacks:write", "comments:write" ], - "client_id_env": "FCODE_GRAPHITE_CLIENT_ID", - "client_secret_env": "FCODE_GRAPHITE_CLIENT_SECRET" + "client_id_env": "FRONTAL_CODE_GRAPHITE_CLIENT_ID", + "client_secret_env": "FRONTAL_CODE_GRAPHITE_CLIENT_SECRET" } }, "slack": { @@ -170,8 +170,8 @@ "chat:write", "channels:read" ], - "client_id_env": "FCODE_SLACK_CLIENT_ID", - "client_secret_env": "FCODE_SLACK_CLIENT_SECRET" + "client_id_env": "FRONTAL_CODE_SLACK_CLIENT_ID", + "client_secret_env": "FRONTAL_CODE_SLACK_CLIENT_SECRET" } } } @@ -179,7 +179,7 @@ "plugins": { "auto_load": [], "enabled": true, - "plugins_dir": "${FCODE_CONFIG_DIR:-${HOME}/.frontal-code}/plugins", + "plugins_dir": "${FRONTAL_CODE_CONFIG_DIR:-${HOME}/.frontal-code}/plugins", "registry_url": "https://plugins.frontal-code.ai", "trusted_sources": [ "https://github.com", diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index e3cb7ac..20d99d7 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -17,8 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Rust toolchain - name: Install pinned Rust toolchain + - name: Install pinned Rust toolchain run: rustup show - name: Cache cargo registry diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index a4c53c4..6e9e15f 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -23,8 +23,7 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} - - name: Rust toolchain - name: Install pinned Rust toolchain + - name: Install pinned Rust toolchain run: rustup show - name: Update dependencies @@ -64,8 +63,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Rust toolchain - name: Install pinned Rust toolchain + - name: Install pinned Rust toolchain run: rustup show - name: Check outdated dependencies @@ -76,5 +74,5 @@ jobs: - name: Check for security advisories run: | - cargo install cargo-audit + cargo install cargo-audit --locked cargo audit diff --git a/.github/workflows/npm-cli.yml b/.github/workflows/npm-cli.yml index 5dcead5..abf540c 100644 --- a/.github/workflows/npm-cli.yml +++ b/.github/workflows/npm-cli.yml @@ -1,6 +1,7 @@ name: Frontal Code CLI npm on: + push: branches: [main] paths: - '.github/workflows/npm-cli.yml' diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index d6d2de4..5781ca9 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -19,8 +19,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Rust toolchain - name: Install pinned Rust toolchain + - name: Install pinned Rust toolchain run: rustup show - name: Set up Node.js @@ -28,10 +27,11 @@ jobs: with: node-version: 20 + - name: Install cargo-audit + run: cargo install cargo-audit --locked + - name: Run cargo audit - uses: rustsec/audit-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} + run: cargo audit - name: Run npm audit for Slack extension working-directory: extensions/frontal-code-slack @@ -49,4 +49,3 @@ jobs: uses: actions/dependency-review-action@v4 with: fail-on-severity: critical - diff --git a/.gitignore b/.gitignore index fa6119d..9035973 100644 --- a/.gitignore +++ b/.gitignore @@ -35,8 +35,9 @@ extensions/frontal-code-slack/node_modules/ # Bazel (Bzlmod) .bazelrc.project coverage/ +!tools/coverage/ +!tools/coverage/** bazel-bin bazel-out bazel-testlogs bazel-frontal-code - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9cd7c45..08d647b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,16 +2,14 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - id: check-yaml + args: [--allow-multiple-documents] - id: check-added-large-files args: ['--maxkb=1000'] - id: check-merge-conflict - id: check-toml - id: check-json - id: check-executables-have-shebangs - - id: check-shebang-scripts-are-executable - id: mixed-line-ending - repo: https://github.com/doublify/pre-commit-rust @@ -21,30 +19,10 @@ repos: args: ['--workspace', '--all-targets', '--all-features'] - id: clippy args: ['--workspace', '--all-targets', '--all-features', '--', '-D', 'warnings'] - - id: rust-fmt + - id: fmt args: ['--'] - - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 - hooks: - - id: codespell - args: - - '--ignore-words-list=crate,serde,clippy,struct,impl,fn,mut,async,await' - - '--skip=*.lock,*.sum,target/,Cargo.lock' - - - repo: https://github.com/r0x0r/pre-commit-rustsec - rev: v1.0.0 - hooks: - - id: cargo-audit - args: ['--workspace'] - # --- Bazel / polyglot tooling (added by the Bzlmod migration) --------------- - - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.41.0 - hooks: - - id: markdownlint - files: '.*\.md$' - - repo: https://github.com/editorconfig-checker/editorconfig-checker.python rev: 2.7.3 hooks: @@ -57,33 +35,6 @@ repos: # --- Biome (Ultracite preset) for TS/JS/JSON/YAML formatting & lint ------ # Replaces prettier: Biome is the single formatter/linter for the monorepo's - # TypeScript and JavaScript surface (including the Frontal Code SDKs). - - repo: local - hooks: - - id: biome-check - name: biome check - entry: bunx @biomejs/biome check --write --no-errors-on-unmatched - language: system - types_or: [file] - files: '\.(ts|tsx|js|jsx|json|jsonc|ya?ml)$' - pass_filenames: false - always_run: true - - - id: biome-format - name: biome format - entry: bunx @biomejs/biome format --write --no-errors-on-unmatched - language: system - types_or: [file] - files: '\.(ts|tsx|js|jsx|json|jsonc|ya?ml)$' - pass_filenames: false - always_run: true - - - repo: https://github.com/hadolint/hadolint - rev: v2.12.0 - hooks: - - id: hadolint - files: '(\.devcontainer/Dockerfile|infrastructure/docker/.*\.Dockerfile|extensions/frontal-code-slack/Dockerfile|sdks/Dockerfile|infrastructure/proxy/Dockerfile)$' - - repo: local hooks: - id: cargo-test @@ -129,12 +80,6 @@ repos: types: [bazel] pass_filenames: false - - id: actionlint - name: actionlint - entry: actionlint - language: system - files: '^\.github/workflows/.*\.(ya?ml)$' - - id: renovate-config-validator name: renovate config validator entry: npx --yes --package renovate -- renovate-config-validator renovate.json diff --git a/BUILD b/BUILD index 9e8b5af..9159d42 100644 --- a/BUILD +++ b/BUILD @@ -1,6 +1,11 @@ package(default_visibility = ["//visibility:public"]) -exports_files(["MODULE.bazel"]) +exports_files([ + "MODULE.bazel", + ".gitignore", + "biome.json", + "ultracite.json", +]) # Buildifier from @buildifier_prebuilt, exposed for Bazel formatter targets. alias( @@ -23,6 +28,7 @@ filegroup( "//:.cargo/config.toml", "//:rust-toolchain.toml", "//crates:all_crates", + "//crates/plugins:bundled_plugins", "//examples:srcs", "//sdks/rust:rust_sdk_srcs", "//tests:all_test_crates", diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 2340a7a..734a04f 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -144,8 +144,8 @@ The main configuration file is `config/project.json`: The system looks for `project.json` in this order: -1. `$FCODE_CONFIG_HOME/project.json` - Custom config directory -2. `$FCODE_HOME/project.json` - Frontal Code home directory +1. `$FRONTAL_CODE_CONFIG_HOME/project.json` - Custom config directory +2. `$FRONTAL_CODE_HOME/project.json` - Frontal Code home directory 3. `~/.frontal-code/project.json` - User's home directory 4. `config/project.json` - Project-local configuration @@ -200,20 +200,20 @@ Environment variables override configuration file settings: ### Core Configuration Overrides ```bash # Runtime settings -export FCODE_DEFAULT_PROVIDER="openai" +export FRONTAL_CODE_DEFAULT_PROVIDER="openai" export RUSTY_CLAUDE_PERMISSION_MODE="workspace-write" -export FCODE_LOG_LEVEL="debug" +export FRONTAL_CODE_LOG_LEVEL="debug" # Feature flags -export FCODE_ENABLE_TELEMETRY="false" -export FCODE_ENABLE_PLUGINS="true" -export FCODE_ENABLE_CACHING="true" +export FRONTAL_CODE_ENABLE_TELEMETRY="false" +export FRONTAL_CODE_ENABLE_PLUGINS="true" +export FRONTAL_CODE_ENABLE_CACHING="true" # Paths -export FCODE_CONFIG_HOME="/custom/config/path" -export FCODE_HOME="/custom/frontal-code/home" -export FCODE_CACHE_DIR="/custom/cache" -export FCODE_LOGS_DIR="/custom/logs" +export FRONTAL_CODE_CONFIG_HOME="/custom/config/path" +export FRONTAL_CODE_HOME="/custom/frontal-code/home" +export FRONTAL_CODE_CACHE_DIR="/custom/cache" +export FRONTAL_CODE_LOGS_DIR="/custom/logs" ``` ### API Provider Variables @@ -238,8 +238,8 @@ export FRONTAL_BASE_URL="https://ai.frontal.dev/v1" ### Service Variables ```bash # Memory/Pinecone -export FCODE_MEMORY_PINECONE_URL="https://index.pinecone.io" -export FCODE_MEMORY_PINECONE_API_KEY="..." +export FRONTAL_CODE_MEMORY_PINECONE_URL="https://index.pinecone.io" +export FRONTAL_CODE_MEMORY_PINECONE_API_KEY="..." ``` ## Legacy Runtime Configuration diff --git a/Cargo.lock b/Cargo.lock index 798a6ad..285a4ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1648,9 +1648,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "bytes", "getrandom 0.3.4", @@ -1897,9 +1897,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "once_cell", "ring", @@ -1921,9 +1921,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -2409,6 +2409,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tools-coverage" +version = "0.1.1" +dependencies = [ + "anyhow", + "clap", + "serde", + "serde_json", +] + [[package]] name = "tools-doctor" version = "0.1.1" diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 76fb7f7..31c0fb2 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -56,9 +56,9 @@ DATABASE_URL=postgresql://frontal-code:frontal-code_password@localhost:5432/fron WEBHOOK_SECRET=your_webhook_secret_here # Configuration overrides -FCODE_LOG_LEVEL=debug -FCODE_PERMISSION_MODE=permissive -FCODE_CONFIG_HOME=./dev-config +FRONTAL_CODE_LOG_LEVEL=debug +FRONTAL_CODE_PERMISSION_MODE=permissive +FRONTAL_CODE_CONFIG_HOME=./dev-config ``` ### Core Configuration Development @@ -76,7 +76,7 @@ let config = ProjectConfig::load_or_default(); let manager = ConfigurationManager::load_with_cwd(".")?; // Enable development-specific features -if std::env::var("FCODE_DEV_MODE").is_ok() { +if std::env::var("FRONTAL_CODE_DEV_MODE").is_ok() { println!("Development mode enabled"); println!("Telemetry: {}", config.features.enable_telemetry); println!("Plugins: {}", config.features.enable_plugins); @@ -89,13 +89,13 @@ Test configuration changes without affecting your main setup: ```bash # Test with custom config directory -export FCODE_CONFIG_HOME=./test-config +export FRONTAL_CODE_CONFIG_HOME=./test-config frontal-code /doctor # Test with specific config file cp config/project.json config/test-project.json # Edit test-project.json -FCODE_CONFIG_HOME=./test-config frontal-code /doctor +FRONTAL_CODE_CONFIG_HOME=./test-config frontal-code /doctor ``` ## Pre-commit Hooks @@ -185,9 +185,9 @@ KEYS * ### Pinecone ```bash # Export Pinecone settings before running memory-backed flows -export FCODE_MEMORY_PINECONE_URL=https://YOUR_INDEX_HOST -export FCODE_MEMORY_PINECONE_API_KEY=your_pinecone_api_key_here -export FCODE_MEMORY_PINECONE_NAMESPACE=dev +export FRONTAL_CODE_MEMORY_PINECONE_URL=https://YOUR_INDEX_HOST +export FRONTAL_CODE_MEMORY_PINECONE_API_KEY=your_pinecone_api_key_here +export FRONTAL_CODE_MEMORY_PINECONE_NAMESPACE=dev # Run the focused tool integration test against a configured backend cargo test -p frontal-code-tools env_backed_memory_tools_route_requests_to_pinecone_and_neo4j -- --nocapture diff --git a/README.md b/README.md index 063804e..866fcec 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ Frontal Code now uses a centralized configuration system located at `config/proj The system looks for `project.json` in this order: -1. `$FCODE_CONFIG_HOME/project.json` - Custom config directory -2. `$FCODE_HOME/project.json` - Frontal Code home directory +1. `$FRONTAL_CODE_CONFIG_HOME/project.json` - Custom config directory +2. `$FRONTAL_CODE_HOME/project.json` - Frontal Code home directory 3. `~/.frontal-code/project.json` - User's home directory 4. `config/project.json` - Project-local configuration diff --git a/USAGE.md b/USAGE.md index ee19b8d..fee406d 100644 --- a/USAGE.md +++ b/USAGE.md @@ -48,8 +48,8 @@ The main configuration file is `config/project.json`. It contains: The system looks for `project.json` in this order: -1. `$FCODE_CONFIG_HOME/project.json` - Custom config directory -2. `$FCODE_HOME/project.json` - Frontal Code home directory +1. `$FRONTAL_CODE_CONFIG_HOME/project.json` - Custom config directory +2. `$FRONTAL_CODE_HOME/project.json` - Frontal Code home directory 3. `~/.frontal-code/project.json` - User's home directory 4. `config/project.json` - Project-local configuration @@ -100,13 +100,13 @@ Environment variables take precedence over configuration file settings: ```bash # Override default provider -export FCODE_DEFAULT_PROVIDER="openai" +export FRONTAL_CODE_DEFAULT_PROVIDER="openai" # Override log level -export FCODE_LOG_LEVEL="debug" +export FRONTAL_CODE_LOG_LEVEL="debug" # Override permission mode -export FCODE_PERMISSION_MODE="restricted" +export FRONTAL_CODE_PERMISSION_MODE="restricted" ``` ## Install / build the workspace diff --git a/biome.json b/biome.json index cc99ffd..615186f 100644 --- a/biome.json +++ b/biome.json @@ -18,6 +18,7 @@ "!**/bazel-out", "!**/bazel-testlogs", "!**/bazel-*", + "!**/src/generated", "!**/*.lock", "!**/*.sum" ] diff --git a/cli/BUILD.bazel b/cli/BUILD.bazel index 4b93d8d..0b4ab45 100644 --- a/cli/BUILD.bazel +++ b/cli/BUILD.bazel @@ -18,7 +18,9 @@ filegroup( sh_test( name = "test", + local = True, srcs = ["//tools:cli.sh"], + data = [":srcs", "//:biome.json", "//:ultracite.json", "//:.gitignore"], args = ["test"], tags = [ "test", @@ -28,7 +30,9 @@ sh_test( sh_test( name = "lint", + size = "small", srcs = ["//tools:cli.sh"], + data = [":srcs", "//:biome.json", "//:ultracite.json", "//:.gitignore"], args = ["lint"], tags = [ "lint", @@ -38,7 +42,10 @@ sh_test( sh_test( name = "build", + local = True, + timeout = "long", srcs = ["//tools:cli.sh"], + data = [":srcs", "//:cargo_workspace", "//:biome.json", "//:ultracite.json", "//:.gitignore"], args = ["build"], tags = [ "build", diff --git a/cli/README.md b/cli/README.md index af32af4..dc3fc9b 100644 --- a/cli/README.md +++ b/cli/README.md @@ -90,8 +90,8 @@ These control the **npm launcher only** (not the agent): | Variable | Effect | | --- | --- | -| `FCODE_SKIP_DOWNLOAD=1` | Skip the postinstall binary download entirely. | -| `FCODE_FORCE_DOWNLOAD=1` | Force (re)download even when a binary already exists or CI skips are set (used by `npm run download`). | +| `FRONTAL_CODE_SKIP_DOWNLOAD=1` | Skip the postinstall binary download entirely. | +| `FRONTAL_CODE_FORCE_DOWNLOAD=1` | Force (re)download even when a binary already exists or CI skips are set (used by `npm run download`). | | `npm_config_offline=true` | Offline install → download is skipped automatically. | ## Supported platforms diff --git a/cli/lib/download.mjs b/cli/lib/download.mjs index 82b1340..56b30fa 100644 --- a/cli/lib/download.mjs +++ b/cli/lib/download.mjs @@ -9,8 +9,8 @@ import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; -import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"; -import { dirname, join } from "node:path"; +import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; import { detectTarget, UnsupportedPlatformError } from "./platform.mjs"; export const RELEASE_BASE = "https://github.com/frontal-labs/frontal-code/releases/download"; @@ -21,7 +21,7 @@ export function releaseAssetUrl(version, assetName) { return `${RELEASE_BASE}/${tag}/${assetName}`; } -export async function fetchUrl(url, { redirects = 5 } = {}) { +export async function fetchUrl(url) { const res = await fetch(url, { redirect: "follow" }); if (res.status === 404) { const err = new Error(`Release asset not found: ${url}`); @@ -61,7 +61,7 @@ function extractTarball(tarballPath, target, destDir) { execFileSync("tar", ["-xzf", tarballPath, "-C", outDir], { stdio: "inherit" }); } -async function downloadTo({ version, destDir, log = () => {} }) { +async function downloadTo({ version, destDir, log = () => undefined }) { const { target, assetName, ext, binName } = detectTarget(); const url = releaseAssetUrl(version, assetName); log(`Downloading ${assetName} (${target}) from ${url}`); @@ -107,7 +107,7 @@ async function downloadTo({ version, destDir, log = () => {} }) { } // Public entry point. Returns the resolved binary path under vendor/. -export async function downloadRelease({ version, destDir, log = () => {} }) { +export async function downloadRelease({ version, destDir, log = () => undefined }) { if (!version || /dev$/.test(version) || version === "0.0.0") { throw new Error( `Refusing to download for dev version "${version}". Use a tagged release version.`, diff --git a/cli/scripts/build.sh b/cli/scripts/build.sh index 7dc25da..e8fff55 100755 --- a/cli/scripts/build.sh +++ b/cli/scripts/build.sh @@ -20,7 +20,7 @@ if ! command -v cargo >/dev/null 2>&1; then fi echo "==> Building cli with cargo (release)" -( cd "$REPO_ROOT" && cargo build --release -p cli ) +( cd "$REPO_ROOT" && cargo build --release -p frontal-code-cli ) # Determine target triple dir name (mirror lib/platform.mjs: darwin/arm64 -> macos-arm64). OS="$(uname -s)"; ARCH="$(uname -m)" diff --git a/cli/scripts/download.sh b/cli/scripts/download.sh index 7c2ed3f..3039502 100755 --- a/cli/scripts/download.sh +++ b/cli/scripts/download.sh @@ -4,5 +4,5 @@ set -euo pipefail PKG_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$PKG_ROOT" -export FCODE_FORCE_DOWNLOAD=1 +export FRONTAL_CODE_FORCE_DOWNLOAD=1 exec ./scripts/postinstall.sh diff --git a/cli/scripts/postinstall.sh b/cli/scripts/postinstall.sh index 42cc965..0559ae9 100755 --- a/cli/scripts/postinstall.sh +++ b/cli/scripts/postinstall.sh @@ -6,7 +6,7 @@ # authority that reports a missing binary to the user at run time. # # Skip conditions (exit 0 quietly): -# - FCODE_SKIP_DOWNLOAD=1 +# - FRONTAL_CODE_SKIP_DOWNLOAD=1 # - npm_config_offline (npm/pnpm/yarn offline) # - npm_config_ignore_scripts (already handled by npm, but be safe) # - dev version (0.0.0-dev / *-dev) -> expect a local cargo build @@ -18,8 +18,8 @@ PKG_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$PKG_ROOT" # Check skip conditions. -if [ "${FCODE_SKIP_DOWNLOAD:-}" = "1" ]; then - echo "[frontal-code:postinstall] skipping download (FCODE_SKIP_DOWNLOAD=1)." +if [ "${FRONTAL_CODE_SKIP_DOWNLOAD:-}" = "1" ]; then + echo "[frontal-code:postinstall] skipping download (FRONTAL_CODE_SKIP_DOWNLOAD=1)." exit 0 fi diff --git a/cli/tests/postinstall.test.mjs b/cli/tests/postinstall.test.mjs index 0b08248..5d3e389 100644 --- a/cli/tests/postinstall.test.mjs +++ b/cli/tests/postinstall.test.mjs @@ -5,7 +5,7 @@ import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { test } from "node:test"; import { fileURLToPath } from "node:url"; -import { detectTarget, UnsupportedPlatformError } from "../lib/platform.mjs"; +import { detectTarget } from "../lib/platform.mjs"; const __dirname = dirname(fileURLToPath(import.meta.url)); const PKG_ROOT = resolve(__dirname, ".."); @@ -14,7 +14,9 @@ const SKIP = process.platform === "win32"; // Run the real postinstall in a throwaway package copy with a fake vendored // binary, asserting dev-version + skip-env short-circuits without network. -test("postinstall skips on FCODE_SKIP_DOWNLOAD without touching network", { skip: SKIP }, () => { +test("postinstall skips on FRONTAL_CODE_SKIP_DOWNLOAD without touching network", { + skip: SKIP, +}, () => { const { target, binName } = detectTarget(); const fakeRoot = mkdtempSync(join(tmpdir(), "frontal-code-pi-")); try { @@ -32,7 +34,7 @@ test("postinstall skips on FCODE_SKIP_DOWNLOAD without touching network", { skip const res = spawnSync("bash", [POSTINSTALL], { cwd: fakeRoot, - env: { ...process.env, FCODE_SKIP_DOWNLOAD: "1", PATH: process.env.PATH }, + env: { ...process.env, FRONTAL_CODE_SKIP_DOWNLOAD: "1", PATH: process.env.PATH }, encoding: "utf8", }); assert.equal(res.status, 0); @@ -71,7 +73,7 @@ test("postinstall fails gracefully (exit 0) when offline download fails", { skip ); const res = spawnSync("bash", [POSTINSTALL], { cwd: fakeRoot, - env: { ...process.env, PATH: process.env.PATH, FCODE_FORCE_DOWNLOAD: "1" }, + env: { ...process.env, PATH: process.env.PATH, FRONTAL_CODE_FORCE_DOWNLOAD: "1" }, encoding: "utf8", timeout: 15000, }); diff --git a/crates/api/.env.example b/crates/api/.env.example index 1bbb924..04bc269 100644 --- a/crates/api/.env.example +++ b/crates/api/.env.example @@ -5,7 +5,7 @@ FRONTAL_API_KEY=your_anthropic_api_key_here FRONTAL_AUTH_TOKEN=your_anthropic_auth_token_here FRONTAL_BASE_URL=https://api.anthropic.com -FCODE_MODEL=claude-opus-5 +FRONTAL_CODE_MODEL=claude-opus-5 # OpenAI Configuration OPENAI_API_KEY=your_openai_api_key_here @@ -35,7 +35,7 @@ OLLAMA_BASE_URL=http://localhost:11434 OLLAMA_MODEL=llama2 # Optional: Override default model selections -# FCODE_MODEL=claude-3-opus-20240229 +# FRONTAL_CODE_MODEL=claude-3-opus-20240229 # OPENAI_MODEL=gpt-4 # XAI_MODEL=grok-3 diff --git a/crates/api/BUILD.bazel b/crates/api/BUILD.bazel index d3677f9..3b89f7c 100644 --- a/crates/api/BUILD.bazel +++ b/crates/api/BUILD.bazel @@ -25,6 +25,7 @@ sh_binary( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-api", "test", @@ -37,7 +38,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-api", "fmt-check", @@ -51,6 +54,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-api", "clippy", diff --git a/crates/api/README.md b/crates/api/README.md index b8a7ee5..64c5781 100644 --- a/crates/api/README.md +++ b/crates/api/README.md @@ -17,10 +17,10 @@ By default it binds to `127.0.0.1:8787`. ## Environment Variables -- `FCODE_API_HOST` (default: `127.0.0.1`) -- `FCODE_API_PORT` (default: `8787`) -- `FCODE_CLI_BIN` (optional path to `frontal-code` binary) -- `FCODE_API_WORKDIR` (optional working directory for executed CLI commands) +- `FRONTAL_CODE_API_HOST` (default: `127.0.0.1`) +- `FRONTAL_CODE_API_PORT` (default: `8787`) +- `FRONTAL_CODE_CLI_BIN` (optional path to `frontal-code` binary) +- `FRONTAL_CODE_API_WORKDIR` (optional working directory for executed CLI commands) - `FRONTAL_API_KEY` (required API key for server authentication; accepts `x-api-key` or `Authorization: Bearer ...`) ## REST Endpoints diff --git a/crates/api/src/service.rs b/crates/api/src/service.rs index 36188b1..31e9264 100644 --- a/crates/api/src/service.rs +++ b/crates/api/src/service.rs @@ -48,23 +48,23 @@ impl ApiServiceConfig { pub fn from_env() -> Result> { let mut config = Self::default(); - if let Ok(host) = env::var("FCODE_API_HOST") { + if let Ok(host) = env::var("FRONTAL_CODE_API_HOST") { let ip: IpAddr = host.parse()?; config.bind_addr = SocketAddr::new(ip, config.bind_addr.port()); } - if let Ok(port) = env::var("FCODE_API_PORT") { + if let Ok(port) = env::var("FRONTAL_CODE_API_PORT") { let parsed: u16 = port.parse()?; config.bind_addr = SocketAddr::new(config.bind_addr.ip(), parsed); } - if let Ok(bin) = env::var("FCODE_CLI_BIN") { + if let Ok(bin) = env::var("FRONTAL_CODE_CLI_BIN") { if !bin.trim().is_empty() { config.cli_bin = Some(PathBuf::from(bin)); } } - if let Ok(workdir) = env::var("FCODE_API_WORKDIR") { + if let Ok(workdir) = env::var("FRONTAL_CODE_API_WORKDIR") { if !workdir.trim().is_empty() { config.working_dir = Some(PathBuf::from(workdir)); } @@ -74,7 +74,7 @@ impl ApiServiceConfig { config.api_key = Some(api_key); } } - if let Ok(allowed) = env::var("FCODE_API_ALLOWED_COMMANDS") { + if let Ok(allowed) = env::var("FRONTAL_CODE_API_ALLOWED_COMMANDS") { let parsed = allowed .split(',') .map(str::trim) @@ -85,14 +85,14 @@ impl ApiServiceConfig { config.allowed_commands = Some(parsed); } } - if let Ok(timeout_ms) = env::var("FCODE_API_COMMAND_TIMEOUT_MS") { + if let Ok(timeout_ms) = env::var("FRONTAL_CODE_API_COMMAND_TIMEOUT_MS") { let parsed: u64 = timeout_ms.parse()?; config.command_timeout_ms = parsed; } - if let Ok(value) = env::var("FCODE_API_ALLOW_INSECURE_BIND") { + if let Ok(value) = env::var("FRONTAL_CODE_API_ALLOW_INSECURE_BIND") { config.allow_insecure_bind = parse_bool_env(&value); } - if let Ok(value) = env::var("FCODE_API_ALLOW_DANGEROUS_PERMISSIONS") { + if let Ok(value) = env::var("FRONTAL_CODE_API_ALLOW_DANGEROUS_PERMISSIONS") { config.allow_dangerous_permissions = parse_bool_env(&value); } @@ -449,7 +449,7 @@ fn resolve_cli_bin(configured: Option) -> PathBuf { return path; } - if let Ok(path) = env::var("FCODE_CLI_BIN") { + if let Ok(path) = env::var("FRONTAL_CODE_CLI_BIN") { if !path.trim().is_empty() { return PathBuf::from(path); } diff --git a/crates/api/tests/client_integration.rs b/crates/api/tests/client_integration.rs index 60b04f1..05fc07b 100644 --- a/crates/api/tests/client_integration.rs +++ b/crates/api/tests/client_integration.rs @@ -696,7 +696,7 @@ async fn send_message_tracks_unexpected_prompt_cache_breaks() { #[tokio::test] async fn live_stream_smoke_test() { - if std::env::var_os("FCODE_RUN_LIVE_TESTS").is_none() { + if std::env::var_os("FRONTAL_CODE_RUN_LIVE_TESTS").is_none() { return; } @@ -707,7 +707,8 @@ async fn live_stream_smoke_test() { }; let mut stream = client .stream_message(&MessageRequest { - model: std::env::var("FCODE_MODEL").unwrap_or_else(|_| "claude-sonnet-4-5".to_string()), + model: std::env::var("FRONTAL_CODE_MODEL") + .unwrap_or_else(|_| "claude-sonnet-4-5".to_string()), max_tokens: 32, messages: vec![InputMessage::user_text( "Reply with exactly: hello from rust", diff --git a/crates/api/tests/service_integration.rs b/crates/api/tests/service_integration.rs index 3c65587..bbac9e0 100644 --- a/crates/api/tests/service_integration.rs +++ b/crates/api/tests/service_integration.rs @@ -33,9 +33,9 @@ impl TestServer { // take it in between. let mut command = Command::new(api_bin); command - .env("FCODE_CLI_BIN", &cli_bin) - .env("FCODE_API_HOST", "127.0.0.1") - .env("FCODE_API_PORT", "0") + .env("FRONTAL_CODE_CLI_BIN", &cli_bin) + .env("FRONTAL_CODE_API_HOST", "127.0.0.1") + .env("FRONTAL_CODE_API_PORT", "0") .stdout(Stdio::piped()); for (key, value) in extra_env { command.env(key, value); @@ -250,7 +250,8 @@ async fn prompt_rejects_danger_full_access_permission_mode_by_default() { #[tokio::test] async fn cli_run_respects_allowed_commands() { let server = - TestServer::start_with_env(&[("FCODE_API_ALLOWED_COMMANDS", "status,version")]).await; + TestServer::start_with_env(&[("FRONTAL_CODE_API_ALLOWED_COMMANDS", "status,version")]) + .await; let client = reqwest::Client::new(); let forbidden = client diff --git a/crates/cli/.env.example b/crates/cli/.env.example index 56759a9..7549a5b 100644 --- a/crates/cli/.env.example +++ b/crates/cli/.env.example @@ -10,7 +10,7 @@ BEDROCK_API_KEY=your_bedrock_api_key_here AZURE_OPENAI_API_KEY=your_azure_openai_api_key_here # Configuration Directories -FCODE_CONFIG_HOME=~/.frontal-code +FRONTAL_CODE_CONFIG_HOME=~/.frontal-code CLAUDE_CONFIG_DIR=~/.claude CLAUDE_CONFIG_HOME=~/.claude CODEX_HOME=~/.codex @@ -18,9 +18,9 @@ CODEX_HOME=~/.codex # Runtime Behavior RUSTY_CLAUDE_PERMISSION_MODE=permissive # Options: permissive, read-only, restricted -FCODE_SANDBOX_ENABLE_DOCKER=0 -# Optional Docker image override when FCODE_SANDBOX_ENABLE_DOCKER=1 -FCODE_SANDBOX_DOCKER_IMAGE=busybox:1.36 +FRONTAL_CODE_SANDBOX_ENABLE_DOCKER=0 +# Optional Docker image override when FRONTAL_CODE_SANDBOX_ENABLE_DOCKER=1 +FRONTAL_CODE_SANDBOX_DOCKER_IMAGE=busybox:1.36 # Logging RUST_LOG=info @@ -37,10 +37,10 @@ MOCK_PARITY_REPORT_PATH=/tmp/mock_parity_report.json # For deployed tools.frontal.dev infrastructure: # Set these environment variables when using the deployed stack: # export FRONTAL_BASE_URL="https://tools.frontal.dev/frontal-code" -# export FCODE_HOSTED_CALLBACK_URL="https://tools.frontal.dev/frontal-code/webhooks/tasks" +# export FRONTAL_CODE_HOSTED_CALLBACK_URL="https://tools.frontal.dev/frontal-code/webhooks/tasks" # Hosted task worker configuration -FCODE_HOSTED_TASK_FILE=/tmp/frontal-code_hosted_task.json +FRONTAL_CODE_HOSTED_TASK_FILE=/tmp/frontal-code_hosted_task.json # Development URL options: # Local development: http://localhost:8788 diff --git a/crates/cli/BUILD.bazel b/crates/cli/BUILD.bazel index 3ca68d2..12fc5d1 100644 --- a/crates/cli/BUILD.bazel +++ b/crates/cli/BUILD.bazel @@ -17,7 +17,7 @@ sh_binary( name = "frontal-code", srcs = ["//tools:crate.sh"], args = [ - "cli", + "frontal-code-cli", "run", ], ) @@ -25,8 +25,9 @@ sh_binary( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ - "cli", + "frontal-code-cli", "test", ], tags = [ @@ -37,9 +38,11 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ - "cli", + "frontal-code-cli", "fmt-check", ], tags = [ @@ -51,8 +54,9 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ - "cli", + "frontal-code-cli", "clippy", ], tags = [ diff --git a/crates/cli/src/init.rs b/crates/cli/src/init.rs index 85369f6..217b6b2 100644 --- a/crates/cli/src/init.rs +++ b/crates/cli/src/init.rs @@ -1,7 +1,7 @@ use std::fs; use std::path::{Path, PathBuf}; -const STARTER_FCODE_JSON: &str = concat!( +const STARTER_FRONTAL_CODE_JSON: &str = concat!( "{\n", " \"permissions\": {\n", " \"defaultMode\": \"dontAsk\"\n", @@ -92,7 +92,7 @@ pub(crate) fn initialize_repo(cwd: &Path) -> Result = option_env!("TARGET"); const GIT_SHA: Option<&str> = option_env!("GIT_SHA"); -const FCODE_TELEMETRY_PATH: &str = "FCODE_TELEMETRY_PATH"; +const FRONTAL_CODE_TELEMETRY_PATH: &str = "FRONTAL_CODE_TELEMETRY_PATH"; const INTERNAL_PROGRESS_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(3); const PRIMARY_SESSION_EXTENSION: &str = "jsonl"; const LEGACY_SESSION_EXTENSION: &str = "json"; @@ -219,7 +219,7 @@ struct TelemetryResolution { fn resolve_telemetry_config( runtime_config: Option<&frontal_code_runtime::RuntimeConfig>, ) -> TelemetryResolution { - if let Ok(path) = env::var(FCODE_TELEMETRY_PATH) { + if let Ok(path) = env::var(FRONTAL_CODE_TELEMETRY_PATH) { let trimmed = path.trim(); if !trimmed.is_empty() { return TelemetryResolution { @@ -3032,16 +3032,16 @@ fn run_hosted_command( } fn hosted_server_url() -> String { - env::var("FCODE_SERVER_URL") + env::var("FRONTAL_CODE_SERVER_URL") .ok() - .or_else(|| env::var("FCODE_SERVER_BASE_URL").ok()) + .or_else(|| env::var("FRONTAL_CODE_SERVER_BASE_URL").ok()) .map(|value| value.trim().trim_end_matches('/').to_string()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| DEFAULT_HOSTED_SERVER_URL.to_string()) } fn hosted_server_api_key() -> Option { - env::var("FCODE_SERVER_API_KEY") + env::var("FRONTAL_CODE_SERVER_API_KEY") .ok() .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) @@ -3280,7 +3280,7 @@ fn update_hosted_task_github( } fn hosted_git_author_name() -> String { - env::var("FCODE_GIT_AUTHOR_NAME") + env::var("FRONTAL_CODE_GIT_AUTHOR_NAME") .ok() .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) @@ -3288,7 +3288,7 @@ fn hosted_git_author_name() -> String { } fn hosted_git_author_email() -> String { - env::var("FCODE_GIT_AUTHOR_EMAIL") + env::var("FRONTAL_CODE_GIT_AUTHOR_EMAIL") .ok() .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) @@ -3670,8 +3670,8 @@ fn run_hosted_task_worker( fn load_hosted_task_worker_payload( task_id: &str, ) -> Result> { - let task_file = env::var("FCODE_HOSTED_TASK_FILE") - .map_err(|_| "FCODE_HOSTED_TASK_FILE must be set for hosted task runs")?; + let task_file = env::var("FRONTAL_CODE_HOSTED_TASK_FILE") + .map_err(|_| "FRONTAL_CODE_HOSTED_TASK_FILE must be set for hosted task runs")?; let payload: HostedTaskWorkerPayload = serde_json::from_slice(&fs::read(task_file)?)?; if payload.task_id != task_id { return Err(format!( @@ -7699,7 +7699,7 @@ fn telemetry_config_file_label(resolution: &TelemetryResolution) -> &'static str } fn telemetry_env_override_value() -> Option { - env::var(FCODE_TELEMETRY_PATH) + env::var(FRONTAL_CODE_TELEMETRY_PATH) .ok() .filter(|value| !value.trim().is_empty()) } @@ -10939,7 +10939,7 @@ mod tests { HostedEventStatus, HostedEventTopic, HostedEventWatchQuery, HostedTaskGithubResponse, HostedTaskListQuery, HostedTaskWorkerPayload, InternalPromptProgressEvent, InternalPromptProgressState, LiveCli, LocalHelpTopic, SlashCommand, StatusUsage, - DEFAULT_MODEL, FCODE_TELEMETRY_PATH, + DEFAULT_MODEL, FRONTAL_CODE_TELEMETRY_PATH, }; use frontal_code_api::{ApiError, MessageResponse, OutputContentBlock, Usage}; use frontal_code_events::EventIdentifiers; @@ -11252,16 +11252,16 @@ mod tests { ) .expect("project config should write"); - let original_config_home = std::env::var("FCODE_CONFIG_HOME").ok(); + let original_config_home = std::env::var("FRONTAL_CODE_CONFIG_HOME").ok(); let original_permission_mode = std::env::var("RUSTY_CLAUDE_PERMISSION_MODE").ok(); - std::env::set_var("FCODE_CONFIG_HOME", &config_home); + std::env::set_var("FRONTAL_CODE_CONFIG_HOME", &config_home); std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE"); let resolved = with_current_dir(&cwd, super::default_permission_mode); match original_config_home { - Some(value) => std::env::set_var("FCODE_CONFIG_HOME", value), - None => std::env::remove_var("FCODE_CONFIG_HOME"), + Some(value) => std::env::set_var("FRONTAL_CODE_CONFIG_HOME", value), + None => std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"), } match original_permission_mode { Some(value) => std::env::set_var("RUSTY_CLAUDE_PERMISSION_MODE", value), @@ -11287,16 +11287,16 @@ mod tests { ) .expect("project config should write"); - let original_config_home = std::env::var("FCODE_CONFIG_HOME").ok(); + let original_config_home = std::env::var("FRONTAL_CODE_CONFIG_HOME").ok(); let original_permission_mode = std::env::var("RUSTY_CLAUDE_PERMISSION_MODE").ok(); - std::env::set_var("FCODE_CONFIG_HOME", &config_home); + std::env::set_var("FRONTAL_CODE_CONFIG_HOME", &config_home); std::env::set_var("RUSTY_CLAUDE_PERMISSION_MODE", "read-only"); let resolved = with_current_dir(&cwd, super::default_permission_mode); match original_config_home { - Some(value) => std::env::set_var("FCODE_CONFIG_HOME", value), - None => std::env::remove_var("FCODE_CONFIG_HOME"), + Some(value) => std::env::set_var("FRONTAL_CODE_CONFIG_HOME", value), + None => std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"), } match original_permission_mode { Some(value) => std::env::set_var("RUSTY_CLAUDE_PERMISSION_MODE", value), @@ -11942,17 +11942,20 @@ mod tests { #[test] fn hosted_server_url_prefers_env_and_trims_trailing_slash() { let _guard = env_lock(); - std::env::set_var("FCODE_SERVER_URL", "http://hosted.frontal-code.test/"); - std::env::remove_var("FCODE_SERVER_BASE_URL"); + std::env::set_var( + "FRONTAL_CODE_SERVER_URL", + "http://hosted.frontal-code.test/", + ); + std::env::remove_var("FRONTAL_CODE_SERVER_BASE_URL"); assert_eq!(hosted_server_url(), "http://hosted.frontal-code.test"); - std::env::remove_var("FCODE_SERVER_URL"); + std::env::remove_var("FRONTAL_CODE_SERVER_URL"); std::env::set_var( - "FCODE_SERVER_BASE_URL", + "FRONTAL_CODE_SERVER_BASE_URL", "http://fallback.frontal-code.test/", ); assert_eq!(hosted_server_url(), "http://fallback.frontal-code.test"); - std::env::remove_var("FCODE_SERVER_BASE_URL"); + std::env::remove_var("FRONTAL_CODE_SERVER_BASE_URL"); } #[test] @@ -11984,7 +11987,7 @@ mod tests { .unwrap(), ) .unwrap(); - std::env::set_var("FCODE_HOSTED_TASK_FILE", &payload_path); + std::env::set_var("FRONTAL_CODE_HOSTED_TASK_FILE", &payload_path); let payload = load_hosted_task_worker_payload("task_123").expect("payload should load successfully"); @@ -12002,7 +12005,7 @@ mod tests { assert_eq!(payload.permission_mode.as_deref(), Some("workspace-write")); assert_eq!(payload.allowed_tools, vec!["git".to_string()]); - std::env::remove_var("FCODE_HOSTED_TASK_FILE"); + std::env::remove_var("FRONTAL_CODE_HOSTED_TASK_FILE"); let _ = fs::remove_dir_all(dir); } @@ -12040,9 +12043,9 @@ mod tests { fn publish_hosted_repo_changes_commits_and_pushes_branch() { let _guard = env_lock(); let previous_token = std::env::var_os("GITHUB_TOKEN"); - let previous_api_base = std::env::var_os("FCODE_GITHUB_API_BASE"); + let previous_api_base = std::env::var_os("FRONTAL_CODE_GITHUB_API_BASE"); std::env::remove_var("GITHUB_TOKEN"); - std::env::remove_var("FCODE_GITHUB_API_BASE"); + std::env::remove_var("FRONTAL_CODE_GITHUB_API_BASE"); let remote = temp_dir(); let repo = temp_dir(); @@ -12117,8 +12120,8 @@ mod tests { None => std::env::remove_var("GITHUB_TOKEN"), } match previous_api_base { - Some(value) => std::env::set_var("FCODE_GITHUB_API_BASE", value), - None => std::env::remove_var("FCODE_GITHUB_API_BASE"), + Some(value) => std::env::set_var("FRONTAL_CODE_GITHUB_API_BASE", value), + None => std::env::remove_var("FRONTAL_CODE_GITHUB_API_BASE"), } let _ = fs::remove_dir_all(remote); let _ = fs::remove_dir_all(repo); @@ -12875,20 +12878,20 @@ mod tests { #[test] fn telemetry_resolution_prefers_env_over_config() { let _guard = env_lock(); - std::env::set_var(FCODE_TELEMETRY_PATH, "/tmp/from-env.jsonl"); + std::env::set_var(FRONTAL_CODE_TELEMETRY_PATH, "/tmp/from-env.jsonl"); let config = frontal_code_runtime::RuntimeConfig::empty(); let resolution = resolve_telemetry_config(Some(&config)); assert_eq!(resolution.source, "env"); assert_eq!(resolution.path.as_deref(), Some("/tmp/from-env.jsonl")); - std::env::remove_var(FCODE_TELEMETRY_PATH); + std::env::remove_var(FRONTAL_CODE_TELEMETRY_PATH); } #[test] fn telemetry_report_uses_sectioned_layout() { let _guard = env_lock(); - std::env::remove_var(FCODE_TELEMETRY_PATH); + std::env::remove_var(FRONTAL_CODE_TELEMETRY_PATH); let report = render_telemetry_report(None).expect("telemetry report should render"); assert!(report.contains("Telemetry")); assert!(report.contains("Enabled")); @@ -12900,7 +12903,7 @@ mod tests { #[test] fn telemetry_report_shows_highest_precedence_config_file() { let _guard = env_lock(); - std::env::remove_var(FCODE_TELEMETRY_PATH); + std::env::remove_var(FRONTAL_CODE_TELEMETRY_PATH); let cwd = temp_dir(); fs::create_dir_all(cwd.join(".frontal-code")).expect("frontal-code dir should exist"); fs::write( @@ -12933,7 +12936,7 @@ mod tests { r#"{"telemetry":{"enabled":true,"path":"local/log.jsonl"}}"#, ) .expect("local settings"); - std::env::set_var(FCODE_TELEMETRY_PATH, "/tmp/from-env.jsonl"); + std::env::set_var(FRONTAL_CODE_TELEMETRY_PATH, "/tmp/from-env.jsonl"); let report = with_current_dir(&cwd, || { render_telemetry_report(None).expect("telemetry report should render") @@ -12943,14 +12946,14 @@ mod tests { assert!(report.contains(".frontal-code/settings.local.json")); assert!(report.contains("Env override /tmp/from-env.jsonl")); - std::env::remove_var(FCODE_TELEMETRY_PATH); + std::env::remove_var(FRONTAL_CODE_TELEMETRY_PATH); fs::remove_dir_all(cwd).expect("cleanup temp dir"); } #[test] fn telemetry_report_status_target_shows_requested_scope_details() { let _guard = env_lock(); - std::env::remove_var(FCODE_TELEMETRY_PATH); + std::env::remove_var(FRONTAL_CODE_TELEMETRY_PATH); let cwd = temp_dir(); fs::create_dir_all(cwd.join(".frontal-code")).expect("frontal-code dir should exist"); fs::write( @@ -12980,7 +12983,7 @@ mod tests { #[test] fn config_telemetry_report_shows_effective_precedence_details() { let _guard = env_lock(); - std::env::remove_var(FCODE_TELEMETRY_PATH); + std::env::remove_var(FRONTAL_CODE_TELEMETRY_PATH); let cwd = temp_dir(); fs::create_dir_all(cwd.join(".frontal-code")).expect("frontal-code dir should exist"); fs::write( @@ -13015,7 +13018,7 @@ mod tests { r#"{"telemetry":{"enabled":true,"path":"local/log.jsonl"}}"#, ) .expect("local settings"); - std::env::set_var(FCODE_TELEMETRY_PATH, "/tmp/from-env.jsonl"); + std::env::set_var(FRONTAL_CODE_TELEMETRY_PATH, "/tmp/from-env.jsonl"); let report = with_current_dir(&cwd, || { render_config_report(Some("telemetry")).expect("telemetry config report should render") @@ -13025,7 +13028,7 @@ mod tests { assert!(report.contains(".frontal-code/settings.local.json")); assert!(report.contains("Env override /tmp/from-env.jsonl")); - std::env::remove_var(FCODE_TELEMETRY_PATH); + std::env::remove_var(FRONTAL_CODE_TELEMETRY_PATH); fs::remove_dir_all(cwd).expect("cleanup temp dir"); } @@ -13092,7 +13095,7 @@ mod tests { #[test] fn config_telemetry_json_includes_effective_resolution_details() { let _guard = env_lock(); - std::env::remove_var(FCODE_TELEMETRY_PATH); + std::env::remove_var(FRONTAL_CODE_TELEMETRY_PATH); let cwd = temp_dir(); fs::create_dir_all(cwd.join(".frontal-code")).expect("frontal-code dir should exist"); fs::write( @@ -13135,7 +13138,7 @@ mod tests { r#"{"telemetry":{"enabled":true,"path":"local/log.jsonl"}}"#, ) .expect("local settings"); - std::env::set_var(FCODE_TELEMETRY_PATH, "/tmp/from-env.jsonl"); + std::env::set_var(FRONTAL_CODE_TELEMETRY_PATH, "/tmp/from-env.jsonl"); let value = with_current_dir(&cwd, || { config_json_value(Some("telemetry")).expect("telemetry config json should render") @@ -13148,14 +13151,14 @@ mod tests { .expect("config path") .ends_with(".frontal-code/settings.local.json")); - std::env::remove_var(FCODE_TELEMETRY_PATH); + std::env::remove_var(FRONTAL_CODE_TELEMETRY_PATH); fs::remove_dir_all(cwd).expect("cleanup temp dir"); } #[test] fn telemetry_status_json_includes_requested_target_details() { let _guard = env_lock(); - std::env::remove_var(FCODE_TELEMETRY_PATH); + std::env::remove_var(FRONTAL_CODE_TELEMETRY_PATH); let cwd = temp_dir(); fs::create_dir_all(cwd.join(".frontal-code")).expect("frontal-code dir should exist"); fs::write( @@ -13190,7 +13193,7 @@ mod tests { #[test] fn config_json_marks_supported_sections_as_unset_when_missing() { let _guard = env_lock(); - std::env::remove_var(FCODE_TELEMETRY_PATH); + std::env::remove_var(FRONTAL_CODE_TELEMETRY_PATH); let cwd = temp_dir(); fs::create_dir_all(cwd.join(".frontal-code")).expect("frontal-code dir should exist"); fs::write( @@ -13235,7 +13238,7 @@ mod tests { #[test] fn telemetry_update_writes_project_settings_json() { let _guard = env_lock(); - std::env::remove_var(FCODE_TELEMETRY_PATH); + std::env::remove_var(FRONTAL_CODE_TELEMETRY_PATH); let cwd = temp_dir(); fs::create_dir_all(&cwd).expect("cwd should exist"); @@ -13263,7 +13266,7 @@ mod tests { #[test] fn telemetry_update_preserves_existing_path_when_disabling() { let _guard = env_lock(); - std::env::remove_var(FCODE_TELEMETRY_PATH); + std::env::remove_var(FRONTAL_CODE_TELEMETRY_PATH); let cwd = temp_dir(); fs::create_dir_all(cwd.join(".frontal-code")).expect("frontal-code dir should exist"); fs::write( @@ -13286,7 +13289,7 @@ mod tests { #[test] fn telemetry_update_can_write_local_settings_json() { let _guard = env_lock(); - std::env::remove_var(FCODE_TELEMETRY_PATH); + std::env::remove_var(FRONTAL_CODE_TELEMETRY_PATH); let cwd = temp_dir(); fs::create_dir_all(&cwd).expect("cwd should exist"); diff --git a/crates/cli/tests/cli_flags_and_config_defaults.rs b/crates/cli/tests/cli_flags_and_config_defaults.rs index 0bd22be..5c6d9ff 100644 --- a/crates/cli/tests/cli_flags_and_config_defaults.rs +++ b/crates/cli/tests/cli_flags_and_config_defaults.rs @@ -153,7 +153,7 @@ fn config_command_loads_defaults_from_standard_config_locations() { // when let output = command_in(&temp_dir) - .env("FCODE_CONFIG_HOME", &config_home) + .env("FRONTAL_CODE_CONFIG_HOME", &config_home) .env("HOME", temp_dir.join("home")) .args([ "--resume", @@ -203,7 +203,7 @@ fn doctor_command_runs_as_a_local_shell_entrypoint() { // when let output = command_in(&temp_dir) - .env("FCODE_CONFIG_HOME", &config_home) + .env("FRONTAL_CODE_CONFIG_HOME", &config_home) .env_remove("FRONTAL_API_KEY") .env_remove("FRONTAL_AUTH_TOKEN") .env("FRONTAL_BASE_URL", "http://127.0.0.1:9") @@ -231,7 +231,7 @@ fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() { fs::create_dir_all(&config_home).expect("config home should exist"); let doctor_help = command_in(&temp_dir) - .env("FCODE_CONFIG_HOME", &config_home) + .env("FRONTAL_CODE_CONFIG_HOME", &config_home) .env_remove("FRONTAL_API_KEY") .env_remove("FRONTAL_AUTH_TOKEN") .env("FRONTAL_BASE_URL", "http://127.0.0.1:9") @@ -239,7 +239,7 @@ fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() { .output() .expect("doctor help should launch"); let status_help = command_in(&temp_dir) - .env("FCODE_CONFIG_HOME", &config_home) + .env("FRONTAL_CODE_CONFIG_HOME", &config_home) .env_remove("FRONTAL_API_KEY") .env_remove("FRONTAL_AUTH_TOKEN") .env("FRONTAL_BASE_URL", "http://127.0.0.1:9") diff --git a/crates/cli/tests/mock_parity_harness.rs b/crates/cli/tests/mock_parity_harness.rs index 7ac7df5..9d54e06 100644 --- a/crates/cli/tests/mock_parity_harness.rs +++ b/crates/cli/tests/mock_parity_harness.rs @@ -294,7 +294,7 @@ fn run_case(case: ScenarioCase, workspace: &HarnessWorkspace, base_url: &str) -> .env_clear() .env("FRONTAL_API_KEY", "test-parity-key") .env("FRONTAL_BASE_URL", base_url) - .env("FCODE_CONFIG_HOME", &workspace.config_home) + .env("FRONTAL_CODE_CONFIG_HOME", &workspace.config_home) .env("HOME", &workspace.home) .env("NO_COLOR", "1") .env("PATH", "/usr/bin:/bin") @@ -407,7 +407,7 @@ fn prepare_plugin_fixture(workspace: &HarnessWorkspace) { let script_path = tool_dir.join("echo-json.sh"); fs::write( &script_path, - "#!/bin/sh\nINPUT=$(cat)\nprintf '{\"plugin\":\"%s\",\"tool\":\"%s\",\"input\":%s}\\n' \"$FCODE_PLUGIN_ID\" \"$FCODE_TOOL_NAME\" \"$INPUT\"\n", + "#!/bin/sh\nINPUT=$(cat)\nprintf '{\"plugin\":\"%s\",\"tool\":\"%s\",\"input\":%s}\\n' \"$FRONTAL_CODE_PLUGIN_ID\" \"$FRONTAL_CODE_TOOL_NAME\" \"$INPUT\"\n", ) .expect("plugin script should write"); let mut permissions = fs::metadata(&script_path) diff --git a/crates/cli/tests/output_format_contract.rs b/crates/cli/tests/output_format_contract.rs index 8ebb170..3765e19 100644 --- a/crates/cli/tests/output_format_contract.rs +++ b/crates/cli/tests/output_format_contract.rs @@ -225,7 +225,7 @@ fn inventory_commands_emit_structured_json_when_requested() { &[ ("HOME", isolated_home.to_str().expect("utf8 home")), ( - "FCODE_CONFIG_HOME", + "FRONTAL_CODE_CONFIG_HOME", isolated_config.to_str().expect("utf8 config home"), ), ( @@ -290,7 +290,7 @@ fn agents_command_emits_structured_agent_entries_when_requested() { &[ ("HOME", home.to_str().expect("utf8 home")), ( - "FCODE_CONFIG_HOME", + "FRONTAL_CODE_CONFIG_HOME", isolated_config.to_str().expect("utf8 config home"), ), ( @@ -460,7 +460,7 @@ fn resumed_inventory_commands_emit_structured_json_when_requested() { ], &[ ( - "FCODE_CONFIG_HOME", + "FRONTAL_CODE_CONFIG_HOME", config_home.to_str().expect("utf8 config home"), ), ("HOME", home.to_str().expect("utf8 home")), @@ -481,7 +481,7 @@ fn resumed_inventory_commands_emit_structured_json_when_requested() { ], &[ ( - "FCODE_CONFIG_HOME", + "FRONTAL_CODE_CONFIG_HOME", config_home.to_str().expect("utf8 config home"), ), ("HOME", home.to_str().expect("utf8 home")), @@ -504,7 +504,7 @@ fn resumed_inventory_commands_emit_structured_json_when_requested() { ], &[ ( - "FCODE_CONFIG_HOME", + "FRONTAL_CODE_CONFIG_HOME", config_home.to_str().expect("utf8 config home"), ), ("HOME", home.to_str().expect("utf8 home")), diff --git a/crates/cli/tests/resume_slash_commands.rs b/crates/cli/tests/resume_slash_commands.rs index 6113b4a..913d8c0 100644 --- a/crates/cli/tests/resume_slash_commands.rs +++ b/crates/cli/tests/resume_slash_commands.rs @@ -147,7 +147,7 @@ fn resumed_config_command_loads_settings_files_end_to_end() { "model", ], &[( - "FCODE_CONFIG_HOME", + "FRONTAL_CODE_CONFIG_HOME", config_home.to_str().expect("utf8 path"), )], ); diff --git a/crates/commands/.env.example b/crates/commands/.env.example index eb5a9c1..5d3c02e 100644 --- a/crates/commands/.env.example +++ b/crates/commands/.env.example @@ -2,13 +2,13 @@ # Copy this file to .env and customize for your commands environment # Configuration Directories for Skills and Commands -FCODE_CONFIG_HOME=~/.frontal-code +FRONTAL_CODE_CONFIG_HOME=~/.frontal-code CODEX_HOME=~/.codex CLAUDE_CONFIG_DIR=~/.claude HOME=~/ # These directories are used to locate and load skills/commands: -# - FCODE_CONFIG_HOME/skills/ +# - FRONTAL_CODE_CONFIG_HOME/skills/ # - CODEX_HOME/skills/ # - CLAUDE_CONFIG_DIR/skills/ # - HOME/.frontal-code/skills/ @@ -16,5 +16,5 @@ HOME=~/ # - HOME/.claude/skills/ # Default skill installation location will be: -# FCODE_CONFIG_HOME/skills/ (if set) or +# FRONTAL_CODE_CONFIG_HOME/skills/ (if set) or # HOME/.frontal-code/skills/ (fallback) diff --git a/crates/commands/BUILD.bazel b/crates/commands/BUILD.bazel index fe7ad7a..f2be851 100644 --- a/crates/commands/BUILD.bazel +++ b/crates/commands/BUILD.bazel @@ -11,6 +11,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-commands", "test", @@ -23,7 +24,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-commands", "fmt-check", @@ -37,6 +40,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-commands", "clippy", diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 36f754a..86d71ec 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -2607,7 +2607,7 @@ fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, P ); } - if let Ok(frontal_code_config_home) = env::var("FCODE_CONFIG_HOME") { + if let Ok(frontal_code_config_home) = env::var("FRONTAL_CODE_CONFIG_HOME") { push_unique_root( &mut roots, DefinitionSource::UserFrontalCodeConfigHome, @@ -2708,7 +2708,7 @@ fn discover_skill_roots(cwd: &Path) -> Vec { ); } - if let Ok(frontal_code_config_home) = env::var("FCODE_CONFIG_HOME") { + if let Ok(frontal_code_config_home) = env::var("FRONTAL_CODE_CONFIG_HOME") { let frontal_code_config_home = PathBuf::from(frontal_code_config_home); push_unique_skill_root( &mut roots, @@ -2869,7 +2869,7 @@ fn install_skill_into( } fn default_skill_install_root() -> std::io::Result { - if let Ok(frontal_code_config_home) = env::var("FCODE_CONFIG_HOME") { + if let Ok(frontal_code_config_home) = env::var("FRONTAL_CODE_CONFIG_HOME") { return Ok(PathBuf::from(frontal_code_config_home).join("skills")); } if let Ok(codex_home) = env::var("CODEX_HOME") { @@ -2880,7 +2880,7 @@ fn default_skill_install_root() -> std::io::Result { } Err(std::io::Error::new( std::io::ErrorKind::NotFound, - "unable to resolve a skills install root; set FCODE_CONFIG_HOME or HOME", + "unable to resolve a skills install root; set FRONTAL_CODE_CONFIG_HOME or HOME", )) } @@ -3572,7 +3572,7 @@ fn render_agents_usage(unexpected: Option<&str>) -> String { "Agents".to_string(), " Usage /agents [list|help]".to_string(), " Direct CLI frontal-code agents".to_string(), - " Sources .frontal-code/agents, ~/.frontal-code/agents, $FCODE_CONFIG_HOME/agents".to_string(), + " Sources .frontal-code/agents, ~/.frontal-code/agents, $FRONTAL_CODE_CONFIG_HOME/agents".to_string(), ]; if let Some(args) = unexpected { lines.push(format!(" Unexpected {args}")); @@ -3587,7 +3587,7 @@ fn render_agents_usage_json(unexpected: Option<&str>) -> Value { "usage": { "slash_command": "/agents [list|help]", "direct_cli": "frontal-code agents [list|help]", - "sources": [".frontal-code/agents", "~/.frontal-code/agents", "$FCODE_CONFIG_HOME/agents"], + "sources": [".frontal-code/agents", "~/.frontal-code/agents", "$FRONTAL_CODE_CONFIG_HOME/agents"], }, "unexpected": unexpected, }) @@ -3600,7 +3600,7 @@ fn render_skills_usage(unexpected: Option<&str>) -> String { " Alias /skill".to_string(), " Direct CLI frontal-code skills [list|install |help| [args]]".to_string(), " Invoke /skills help overview -> $help overview".to_string(), - " Install root $FCODE_CONFIG_HOME/skills or ~/.frontal-code/skills".to_string(), + " Install root $FRONTAL_CODE_CONFIG_HOME/skills or ~/.frontal-code/skills".to_string(), " Sources .frontal-code/skills, .omc/skills, .agents/skills, .codex/skills, .claude/skills, ~/.frontal-code/skills, ~/.omc/skills, ~/.claude/skills/omc-learned, ~/.codex/skills, ~/.claude/skills, legacy /commands".to_string(), ]; if let Some(args) = unexpected { @@ -3618,7 +3618,7 @@ fn render_skills_usage_json(unexpected: Option<&str>) -> Value { "aliases": ["/skill"], "direct_cli": "frontal-code skills [list|install |help| [args]]", "invoke": "/skills help overview -> $help overview", - "install_root": "$FCODE_CONFIG_HOME/skills or ~/.frontal-code/skills", + "install_root": "$FRONTAL_CODE_CONFIG_HOME/skills or ~/.frontal-code/skills", "sources": [ ".frontal-code/skills", ".omc/skills", @@ -4970,7 +4970,7 @@ mod tests { assert!(agents_help.contains("Usage /agents [list|help]")); assert!(agents_help.contains("Direct CLI frontal-code agents")); assert!(agents_help.contains( - "Sources .frontal-code/agents, ~/.frontal-code/agents, $FCODE_CONFIG_HOME/agents" + "Sources .frontal-code/agents, ~/.frontal-code/agents, $FRONTAL_CODE_CONFIG_HOME/agents" )); let agents_unexpected = @@ -4983,8 +4983,9 @@ mod tests { .contains("Usage /skills [list|install |help| [args]]")); assert!(skills_help.contains("Alias /skill")); assert!(skills_help.contains("Invoke /skills help overview -> $help overview")); - assert!(skills_help - .contains("Install root $FCODE_CONFIG_HOME/skills or ~/.frontal-code/skills")); + assert!(skills_help.contains( + "Install root $FRONTAL_CODE_CONFIG_HOME/skills or ~/.frontal-code/skills" + )); assert!(skills_help.contains(".omc/skills")); assert!(skills_help.contains(".agents/skills")); assert!(skills_help.contains("~/.claude/skills/omc-learned")); diff --git a/crates/core/BUILD.bazel b/crates/core/BUILD.bazel index 1e279e1..aff1925 100644 --- a/crates/core/BUILD.bazel +++ b/crates/core/BUILD.bazel @@ -30,6 +30,7 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], data = ["//:cargo_workspace"], args = [ diff --git a/crates/core/README.md b/crates/core/README.md index 87a7c27..f21f78e 100644 --- a/crates/core/README.md +++ b/crates/core/README.md @@ -106,8 +106,8 @@ The core crate follows these principles: The configuration system looks for `project.json` in the following locations (in order): -1. `$FCODE_CONFIG_HOME/project.json` - Custom config directory -2. `$FCODE_HOME/project.json` - Frontal Code home directory +1. `$FRONTAL_CODE_CONFIG_HOME/project.json` - Custom config directory +2. `$FRONTAL_CODE_HOME/project.json` - Frontal Code home directory 3. `~/.frontal-code/project.json` - User's home directory 4. `config/project.json` - Project-local configuration diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 4c73e2f..9d4c55e 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -227,16 +227,16 @@ impl ProjectConfig { /// Get the default configuration file path pub fn get_config_path() -> Result> { - // Check for FCODE_CONFIG_HOME environment variable first - if let Ok(config_home) = env::var("FCODE_CONFIG_HOME") { + // Check for FRONTAL_CODE_CONFIG_HOME environment variable first + if let Ok(config_home) = env::var("FRONTAL_CODE_CONFIG_HOME") { let config_path = PathBuf::from(config_home).join("project.json"); if config_path.exists() { return Ok(config_path); } } - // Check for FCODE_HOME environment variable - if let Ok(frontal_code_home) = env::var("FCODE_HOME") { + // Check for FRONTAL_CODE_HOME environment variable + if let Ok(frontal_code_home) = env::var("FRONTAL_CODE_HOME") { let config_path = PathBuf::from(frontal_code_home).join("project.json"); if config_path.exists() { return Ok(config_path); diff --git a/crates/embeddings/BUILD.bazel b/crates/embeddings/BUILD.bazel index 4635c2e..0ed1ff6 100644 --- a/crates/embeddings/BUILD.bazel +++ b/crates/embeddings/BUILD.bazel @@ -16,6 +16,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-embeddings", "test", @@ -28,7 +29,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-embeddings", "fmt-check", @@ -42,6 +45,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-embeddings", "clippy", diff --git a/crates/events/BUILD.bazel b/crates/events/BUILD.bazel index 32306a9..98f2fb6 100644 --- a/crates/events/BUILD.bazel +++ b/crates/events/BUILD.bazel @@ -16,6 +16,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-events", "test", @@ -28,7 +29,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-events", "fmt-check", @@ -42,6 +45,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-events", "clippy", diff --git a/crates/harness/BUILD.bazel b/crates/harness/BUILD.bazel index 49e1176..bdabe9b 100644 --- a/crates/harness/BUILD.bazel +++ b/crates/harness/BUILD.bazel @@ -11,6 +11,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-harness", "test", @@ -23,7 +24,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-harness", "fmt-check", @@ -37,6 +40,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-harness", "clippy", diff --git a/crates/integrations/BUILD.bazel b/crates/integrations/BUILD.bazel index 546c137..fdd6301 100644 --- a/crates/integrations/BUILD.bazel +++ b/crates/integrations/BUILD.bazel @@ -11,6 +11,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-integrations", "test", @@ -23,7 +24,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-integrations", "fmt-check", @@ -37,6 +40,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-integrations", "clippy", diff --git a/crates/integrations/src/ide.rs b/crates/integrations/src/ide.rs index 2f63c8b..33c8835 100644 --- a/crates/integrations/src/ide.rs +++ b/crates/integrations/src/ide.rs @@ -10,9 +10,9 @@ use zip::write::SimpleFileOptions; use zip::CompressionMethod; use zip::ZipWriter; -const FCODE_CONFIG_DIR: &str = ".frontal-code"; +const FRONTAL_CODE_CONFIG_DIR: &str = ".frontal-code"; const IDE_CONFIG_FILE: &str = "ide.json"; -const FCODE_EXTENSION_DIR: &str = "extensions/frontal-code-ide"; +const FRONTAL_CODE_EXTENSION_DIR: &str = "extensions/frontal-code-ide"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] @@ -259,10 +259,14 @@ pub fn setup_editor_integration( pub fn package_extension(workspace_root: &Path) -> Result { let extension_path = find_extension_dev_path(workspace_root).ok_or_else(|| { - IdeIntegrationError::ExtensionSourceNotFound(workspace_root.join(FCODE_EXTENSION_DIR)) + IdeIntegrationError::ExtensionSourceNotFound( + workspace_root.join(FRONTAL_CODE_EXTENSION_DIR), + ) })?; let metadata = read_extension_metadata(&extension_path)?; - let package_dir = workspace_root.join(FCODE_CONFIG_DIR).join("extensions"); + let package_dir = workspace_root + .join(FRONTAL_CODE_CONFIG_DIR) + .join("extensions"); fs::create_dir_all(&package_dir)?; let package_path = package_dir.join(format!("frontal-code-ide-{}.vsix", metadata.version)); write_vsix_archive(&extension_path, &metadata, &package_path)?; @@ -270,11 +274,15 @@ pub fn package_extension(workspace_root: &Path) -> Result PathBuf { - workspace_root.join(FCODE_CONFIG_DIR).join(IDE_CONFIG_FILE) + workspace_root + .join(FRONTAL_CODE_CONFIG_DIR) + .join(IDE_CONFIG_FILE) } fn find_packaged_extension(workspace_root: &Path) -> Option { - let dir = workspace_root.join(FCODE_CONFIG_DIR).join("extensions"); + let dir = workspace_root + .join(FRONTAL_CODE_CONFIG_DIR) + .join("extensions"); let entries = fs::read_dir(dir).ok()?; entries .filter_map(|entry| entry.ok().map(|value| value.path())) @@ -385,7 +393,7 @@ fn macos_fallback_binary(target: IdeTarget) -> Option { fn find_extension_dev_path(workspace_root: &Path) -> Option { for base in workspace_root.ancestors() { - let candidate = base.join(FCODE_EXTENSION_DIR); + let candidate = base.join(FRONTAL_CODE_EXTENSION_DIR); if candidate.join("package.json").is_file() { return Some(candidate); } diff --git a/crates/integrations/src/mcp/stdio.rs b/crates/integrations/src/mcp/stdio.rs index a6bee9c..47445be 100644 --- a/crates/integrations/src/mcp/stdio.rs +++ b/crates/integrations/src/mcp/stdio.rs @@ -2429,7 +2429,7 @@ mod tests { let mut manager = McpServerManager::from_servers(&servers); manager.discover_tools().await.expect("discover tools"); - tokio::time::sleep(std::time::Duration::from_millis(2000)).await; + tokio::time::sleep(std::time::Duration::from_secs(2)).await; let first_response = manager .call_tool( &mcp_tool_name("alpha", "echo"), diff --git a/crates/memory/BUILD.bazel b/crates/memory/BUILD.bazel index 0717716..605d202 100644 --- a/crates/memory/BUILD.bazel +++ b/crates/memory/BUILD.bazel @@ -16,6 +16,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-memory", "test", @@ -28,7 +29,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-memory", "fmt-check", @@ -42,6 +45,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-memory", "clippy", diff --git a/crates/memory/README.md b/crates/memory/README.md index 6af9106..84dd67f 100644 --- a/crates/memory/README.md +++ b/crates/memory/README.md @@ -27,11 +27,11 @@ This crate powers memory tools such as `MemoryUpsert`, `MemorySearch`, and `Know Runtime backend selection is environment-driven: -- `FCODE_MEMORY_METADATA_PATH` enables `PersistentFileMetadataStore` -- `FCODE_MEMORY_PINECONE_URL` points at the Pinecone index host -- `FCODE_MEMORY_PINECONE_API_KEY` adds Pinecone API-key auth -- `FCODE_MEMORY_PINECONE_NAMESPACE` prefixes per-scope namespaces inside Pinecone -- `FCODE_MEMORY_NEO4J_URL` + `FCODE_MEMORY_NEO4J_DATABASE` enable Neo4j KG storage -- `FCODE_MEMORY_NEO4J_USERNAME` + `FCODE_MEMORY_NEO4J_PASSWORD` add Neo4j basic auth +- `FRONTAL_CODE_MEMORY_METADATA_PATH` enables `PersistentFileMetadataStore` +- `FRONTAL_CODE_MEMORY_PINECONE_URL` points at the Pinecone index host +- `FRONTAL_CODE_MEMORY_PINECONE_API_KEY` adds Pinecone API-key auth +- `FRONTAL_CODE_MEMORY_PINECONE_NAMESPACE` prefixes per-scope namespaces inside Pinecone +- `FRONTAL_CODE_MEMORY_NEO4J_URL` + `FRONTAL_CODE_MEMORY_NEO4J_DATABASE` enable Neo4j KG storage +- `FRONTAL_CODE_MEMORY_NEO4J_USERNAME` + `FRONTAL_CODE_MEMORY_NEO4J_PASSWORD` add Neo4j basic auth If those variables are absent, the crate falls back to in-memory metadata/vector/graph stores. diff --git a/crates/memory/src/lib.rs b/crates/memory/src/lib.rs index 6dd458e..d3679b1 100644 --- a/crates/memory/src/lib.rs +++ b/crates/memory/src/lib.rs @@ -24,14 +24,14 @@ pub use vector_pinecone_adapter::{ ReqwestPineconeTransport, }; -const FCODE_MEMORY_METADATA_PATH: &str = "FCODE_MEMORY_METADATA_PATH"; -const FCODE_MEMORY_PINECONE_URL: &str = "FCODE_MEMORY_PINECONE_URL"; -const FCODE_MEMORY_PINECONE_NAMESPACE: &str = "FCODE_MEMORY_PINECONE_NAMESPACE"; -const FCODE_MEMORY_PINECONE_API_KEY: &str = "FCODE_MEMORY_PINECONE_API_KEY"; -const FCODE_MEMORY_NEO4J_URL: &str = "FCODE_MEMORY_NEO4J_URL"; -const FCODE_MEMORY_NEO4J_DATABASE: &str = "FCODE_MEMORY_NEO4J_DATABASE"; -const FCODE_MEMORY_NEO4J_USERNAME: &str = "FCODE_MEMORY_NEO4J_USERNAME"; -const FCODE_MEMORY_NEO4J_PASSWORD: &str = "FCODE_MEMORY_NEO4J_PASSWORD"; +const FRONTAL_CODE_MEMORY_METADATA_PATH: &str = "FRONTAL_CODE_MEMORY_METADATA_PATH"; +const FRONTAL_CODE_MEMORY_PINECONE_URL: &str = "FRONTAL_CODE_MEMORY_PINECONE_URL"; +const FRONTAL_CODE_MEMORY_PINECONE_NAMESPACE: &str = "FRONTAL_CODE_MEMORY_PINECONE_NAMESPACE"; +const FRONTAL_CODE_MEMORY_PINECONE_API_KEY: &str = "FRONTAL_CODE_MEMORY_PINECONE_API_KEY"; +const FRONTAL_CODE_MEMORY_NEO4J_URL: &str = "FRONTAL_CODE_MEMORY_NEO4J_URL"; +const FRONTAL_CODE_MEMORY_NEO4J_DATABASE: &str = "FRONTAL_CODE_MEMORY_NEO4J_DATABASE"; +const FRONTAL_CODE_MEMORY_NEO4J_USERNAME: &str = "FRONTAL_CODE_MEMORY_NEO4J_USERNAME"; +const FRONTAL_CODE_MEMORY_NEO4J_PASSWORD: &str = "FRONTAL_CODE_MEMORY_NEO4J_PASSWORD"; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct MemoryScope { @@ -235,14 +235,14 @@ impl MemoryBackendConfig { #[must_use] pub fn from_env() -> Self { Self { - metadata_path: env_var(FCODE_MEMORY_METADATA_PATH).map(PathBuf::from), - pinecone_url: env_var(FCODE_MEMORY_PINECONE_URL), - pinecone_namespace: env_var(FCODE_MEMORY_PINECONE_NAMESPACE), - pinecone_api_key: env_var(FCODE_MEMORY_PINECONE_API_KEY), - neo4j_url: env_var(FCODE_MEMORY_NEO4J_URL), - neo4j_database: env_var(FCODE_MEMORY_NEO4J_DATABASE), - neo4j_username: env_var(FCODE_MEMORY_NEO4J_USERNAME), - neo4j_password: env_var(FCODE_MEMORY_NEO4J_PASSWORD), + metadata_path: env_var(FRONTAL_CODE_MEMORY_METADATA_PATH).map(PathBuf::from), + pinecone_url: env_var(FRONTAL_CODE_MEMORY_PINECONE_URL), + pinecone_namespace: env_var(FRONTAL_CODE_MEMORY_PINECONE_NAMESPACE), + pinecone_api_key: env_var(FRONTAL_CODE_MEMORY_PINECONE_API_KEY), + neo4j_url: env_var(FRONTAL_CODE_MEMORY_NEO4J_URL), + neo4j_database: env_var(FRONTAL_CODE_MEMORY_NEO4J_DATABASE), + neo4j_username: env_var(FRONTAL_CODE_MEMORY_NEO4J_USERNAME), + neo4j_password: env_var(FRONTAL_CODE_MEMORY_NEO4J_PASSWORD), } } } diff --git a/crates/orchestrator/BUILD.bazel b/crates/orchestrator/BUILD.bazel index b749bd8..055ad9e 100644 --- a/crates/orchestrator/BUILD.bazel +++ b/crates/orchestrator/BUILD.bazel @@ -16,6 +16,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-orchestrator", "test", @@ -28,7 +29,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-orchestrator", "fmt-check", @@ -42,6 +45,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-orchestrator", "clippy", diff --git a/crates/plugins/BUILD.bazel b/crates/plugins/BUILD.bazel index 4a8ea8c..8865737 100644 --- a/crates/plugins/BUILD.bazel +++ b/crates/plugins/BUILD.bazel @@ -13,9 +13,15 @@ filegroup( srcs = glob(["tests/**/*.rs"]), ) +filegroup( + name = "bundled_plugins", + srcs = glob(["bundled/**/*"]), +) + sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-plugins", "test", @@ -28,7 +34,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-plugins", "fmt-check", @@ -42,6 +50,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-plugins", "clippy", diff --git a/crates/plugins/src/lib.rs b/crates/plugins/src/lib.rs index 63e3553..2ff7904 100644 --- a/crates/plugins/src/lib.rs +++ b/crates/plugins/src/lib.rs @@ -309,14 +309,14 @@ impl PluginTool { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .env("FCODE_PLUGIN_ID", &self.plugin_id) - .env("FCODE_PLUGIN_NAME", &self.plugin_name) - .env("FCODE_TOOL_NAME", &self.definition.name) - .env("FCODE_TOOL_INPUT", &input_json); + .env("FRONTAL_CODE_PLUGIN_ID", &self.plugin_id) + .env("FRONTAL_CODE_PLUGIN_NAME", &self.plugin_name) + .env("FRONTAL_CODE_TOOL_NAME", &self.definition.name) + .env("FRONTAL_CODE_TOOL_INPUT", &input_json); if let Some(root) = &self.root { process .current_dir(root) - .env("FCODE_PLUGIN_ROOT", root.display().to_string()); + .env("FRONTAL_CODE_PLUGIN_ROOT", root.display().to_string()); } let mut child = process.spawn()?; @@ -2422,7 +2422,7 @@ mod tests { let script_path = root.join("tools").join("echo-json.sh"); write_file( &script_path, - "#!/bin/sh\nINPUT=$(cat)\nprintf '{\"plugin\":\"%s\",\"tool\":\"%s\",\"input\":%s}\\n' \"$FCODE_PLUGIN_ID\" \"$FCODE_TOOL_NAME\" \"$INPUT\"\n", + "#!/bin/sh\nINPUT=$(cat)\nprintf '{\"plugin\":\"%s\",\"tool\":\"%s\",\"input\":%s}\\n' \"$FRONTAL_CODE_PLUGIN_ID\" \"$FRONTAL_CODE_TOOL_NAME\" \"$INPUT\"\n", ); #[cfg(unix)] { diff --git a/crates/providers/BUILD.bazel b/crates/providers/BUILD.bazel index f9debf9..c345355 100644 --- a/crates/providers/BUILD.bazel +++ b/crates/providers/BUILD.bazel @@ -11,6 +11,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-providers", "test", @@ -23,7 +24,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-providers", "fmt-check", @@ -37,6 +40,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-providers", "clippy", diff --git a/crates/providers/src/lib.rs b/crates/providers/src/lib.rs index 10da41a..12274fa 100644 --- a/crates/providers/src/lib.rs +++ b/crates/providers/src/lib.rs @@ -32,5 +32,5 @@ pub use types::{ pub use frontal_code_telemetry::{ AnalyticsEvent, AnthropicRequestProfile, ClientIdentity, JsonlTelemetrySink, MemoryTelemetrySink, SessionTraceRecord, SessionTracer, TelemetryEvent, TelemetrySink, - DEFAULT_FCODE_VERSION, + DEFAULT_FRONTAL_CODE_VERSION, }; diff --git a/crates/providers/src/providers/anthropic.rs b/crates/providers/src/providers/anthropic.rs index 39757a4..442c2f9 100644 --- a/crates/providers/src/providers/anthropic.rs +++ b/crates/providers/src/providers/anthropic.rs @@ -974,7 +974,7 @@ mod tests { let _guard = env_lock(); std::env::remove_var("FRONTAL_AUTH_TOKEN"); std::env::remove_var("FRONTAL_API_KEY"); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); let error = super::read_api_key().expect_err("missing key should error"); assert!(matches!( error, @@ -1044,7 +1044,7 @@ mod tests { fn auth_source_from_saved_oauth_when_env_absent() { let _guard = env_lock(); let config_home = temp_config_home(); - std::env::set_var("FCODE_CONFIG_HOME", &config_home); + std::env::set_var("FRONTAL_CODE_CONFIG_HOME", &config_home); std::env::remove_var("FRONTAL_AUTH_TOKEN"); std::env::remove_var("FRONTAL_API_KEY"); save_oauth_credentials(&frontal_code_runtime::OAuthTokenSet { @@ -1059,7 +1059,7 @@ mod tests { assert_eq!(auth.bearer_token(), Some("saved-access-token")); clear_oauth_credentials().expect("clear credentials"); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); cleanup_temp_config_home(&config_home); } @@ -1083,7 +1083,7 @@ mod tests { fn resolve_saved_oauth_token_refreshes_expired_credentials() { let _guard = env_lock(); let config_home = temp_config_home(); - std::env::set_var("FCODE_CONFIG_HOME", &config_home); + std::env::set_var("FRONTAL_CODE_CONFIG_HOME", &config_home); std::env::remove_var("FRONTAL_AUTH_TOKEN"); std::env::remove_var("FRONTAL_API_KEY"); save_oauth_credentials(&frontal_code_runtime::OAuthTokenSet { @@ -1098,7 +1098,7 @@ mod tests { "{\"access_token\":\"refreshed-token\",\"refresh_token\":\"fresh-refresh\",\"expires_at\":9999999999,\"scopes\":[\"scope:a\"]}", ) else { clear_oauth_credentials().expect("clear credentials"); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); cleanup_temp_config_home(&config_home); return; }; @@ -1112,7 +1112,7 @@ mod tests { assert_eq!(stored.access_token, "refreshed-token"); clear_oauth_credentials().expect("clear credentials"); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); cleanup_temp_config_home(&config_home); } @@ -1120,7 +1120,7 @@ mod tests { fn resolve_startup_auth_source_uses_saved_oauth_without_loading_config() { let _guard = env_lock(); let config_home = temp_config_home(); - std::env::set_var("FCODE_CONFIG_HOME", &config_home); + std::env::set_var("FRONTAL_CODE_CONFIG_HOME", &config_home); std::env::remove_var("FRONTAL_AUTH_TOKEN"); std::env::remove_var("FRONTAL_API_KEY"); save_oauth_credentials(&frontal_code_runtime::OAuthTokenSet { @@ -1136,7 +1136,7 @@ mod tests { assert_eq!(auth.bearer_token(), Some("saved-access-token")); clear_oauth_credentials().expect("clear credentials"); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); cleanup_temp_config_home(&config_home); } @@ -1144,7 +1144,7 @@ mod tests { fn resolve_startup_auth_source_errors_when_refreshable_token_lacks_config() { let _guard = env_lock(); let config_home = temp_config_home(); - std::env::set_var("FCODE_CONFIG_HOME", &config_home); + std::env::set_var("FRONTAL_CODE_CONFIG_HOME", &config_home); std::env::remove_var("FRONTAL_AUTH_TOKEN"); std::env::remove_var("FRONTAL_API_KEY"); save_oauth_credentials(&frontal_code_runtime::OAuthTokenSet { @@ -1168,7 +1168,7 @@ mod tests { assert_eq!(stored.refresh_token.as_deref(), Some("refresh-token")); clear_oauth_credentials().expect("clear credentials"); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); cleanup_temp_config_home(&config_home); } @@ -1176,7 +1176,7 @@ mod tests { fn resolve_saved_oauth_token_preserves_refresh_token_when_refresh_response_omits_it() { let _guard = env_lock(); let config_home = temp_config_home(); - std::env::set_var("FCODE_CONFIG_HOME", &config_home); + std::env::set_var("FRONTAL_CODE_CONFIG_HOME", &config_home); std::env::remove_var("FRONTAL_AUTH_TOKEN"); std::env::remove_var("FRONTAL_API_KEY"); save_oauth_credentials(&frontal_code_runtime::OAuthTokenSet { @@ -1191,7 +1191,7 @@ mod tests { "{\"access_token\":\"refreshed-token\",\"expires_at\":9999999999,\"scopes\":[\"scope:a\"]}", ) else { clear_oauth_credentials().expect("clear credentials"); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); cleanup_temp_config_home(&config_home); return; }; @@ -1206,7 +1206,7 @@ mod tests { assert_eq!(stored.refresh_token.as_deref(), Some("refresh-token")); clear_oauth_credentials().expect("clear credentials"); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); cleanup_temp_config_home(&config_home); } diff --git a/crates/repo/BUILD.bazel b/crates/repo/BUILD.bazel index 3ba7fad..4e1a552 100644 --- a/crates/repo/BUILD.bazel +++ b/crates/repo/BUILD.bazel @@ -11,6 +11,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-repo", "test", @@ -23,7 +24,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-repo", "fmt-check", @@ -36,7 +39,9 @@ sh_test( sh_test( name = "lint", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-repo", "clippy", diff --git a/crates/runtime/.env.example b/crates/runtime/.env.example index b9e1340..d7a7401 100644 --- a/crates/runtime/.env.example +++ b/crates/runtime/.env.example @@ -2,17 +2,17 @@ # Copy this file to .env and customize for your runtime environment # Configuration Directories -FCODE_CONFIG_HOME=~/.frontal-code -FCODE_HOME=~/.frontal-code +FRONTAL_CODE_CONFIG_HOME=~/.frontal-code +FRONTAL_CODE_HOME=~/.frontal-code HOME=~/ # Sandbox Configuration -FCODE_SANDBOX_HOME=~/.frontal-code/sandbox +FRONTAL_CODE_SANDBOX_HOME=~/.frontal-code/sandbox SANDBOX_HOME=~/.frontal-code/sandbox # Enable Docker fallback backend explicitly (default: disabled) -FCODE_SANDBOX_ENABLE_DOCKER=0 -# Optional Docker image override when FCODE_SANDBOX_ENABLE_DOCKER=1 -FCODE_SANDBOX_DOCKER_IMAGE=busybox:1.36 +FRONTAL_CODE_SANDBOX_ENABLE_DOCKER=0 +# Optional Docker image override when FRONTAL_CODE_SANDBOX_ENABLE_DOCKER=1 +FRONTAL_CODE_SANDBOX_DOCKER_IMAGE=busybox:1.36 # System Paths (ensure these are available in your PATH) PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin @@ -21,4 +21,4 @@ PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin # AUTO_COMPACTION_THRESHOLD=100 # OAuth & Authentication Storage -# Credentials will be stored in FCODE_CONFIG_HOME/oauth/ by default +# Credentials will be stored in FRONTAL_CODE_CONFIG_HOME/oauth/ by default diff --git a/crates/runtime/BUILD.bazel b/crates/runtime/BUILD.bazel index 23a56b1..47045fc 100644 --- a/crates/runtime/BUILD.bazel +++ b/crates/runtime/BUILD.bazel @@ -16,6 +16,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-runtime", "test", @@ -28,7 +29,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-runtime", "fmt-check", @@ -42,6 +45,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-runtime", "clippy", diff --git a/crates/runtime/src/config.rs b/crates/runtime/src/config.rs index f818c76..070b14a 100644 --- a/crates/runtime/src/config.rs +++ b/crates/runtime/src/config.rs @@ -7,7 +7,7 @@ use crate::json::JsonValue; use crate::sandbox::{FilesystemIsolationMode, SandboxConfig}; /// Schema name advertised by generated settings files. -pub const FCODE_SETTINGS_SCHEMA_NAME: &str = "SettingsSchema"; +pub const FRONTAL_CODE_SETTINGS_SCHEMA_NAME: &str = "SettingsSchema"; /// Origin of a loaded settings file in the configuration precedence chain. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -508,7 +508,7 @@ impl RuntimeTelemetryConfig { #[must_use] /// Returns the default per-user config directory used by the runtime. pub fn default_config_home() -> PathBuf { - std::env::var_os("FCODE_CONFIG_HOME") + std::env::var_os("FRONTAL_CODE_CONFIG_HOME") .map(PathBuf::from) .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".frontal-code"))) .unwrap_or_else(|| PathBuf::from(".frontal-code")) @@ -747,7 +747,11 @@ fn parse_optional_plugin_config(root: &JsonValue) -> Result), Object(BTreeMap), } +impl Eq for JsonValue {} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct JsonError { message: String, @@ -40,6 +43,7 @@ impl JsonValue { Self::Null => "null".to_string(), Self::Bool(value) => value.to_string(), Self::Number(value) => value.to_string(), + Self::Float(value) => value.to_string(), Self::String(value) => render_string(value), Self::Array(values) => { let rendered = values @@ -110,6 +114,16 @@ impl JsonValue { _ => None, } } + + #[must_use] + #[allow(clippy::cast_precision_loss)] + pub fn as_f64(&self) -> Option { + match self { + Self::Float(value) => Some(*value), + Self::Number(value) => Some(*value as f64), + _ => None, + } + } } fn render_string(value: &str) -> String { @@ -167,7 +181,7 @@ impl<'a> Parser<'a> { Some('"') => self.parse_string().map(JsonValue::String), Some('[') => self.parse_array(), Some('{') => self.parse_object(), - Some('-' | '0'..='9') => self.parse_number().map(JsonValue::Number), + Some('-' | '0'..='9') => self.parse_number(), Some(other) => Err(JsonError::new(format!("unexpected character: {other}"))), None => Err(JsonError::new("unexpected end of input")), } @@ -266,24 +280,83 @@ impl<'a> Parser<'a> { Ok(JsonValue::Object(entries)) } - fn parse_number(&mut self) -> Result { + fn parse_number(&mut self) -> Result { let mut value = String::new(); if self.try_consume('-') { value.push('-'); } - while let Some(ch @ '0'..='9') = self.peek() { - value.push(ch); - self.index += 1; + match self.peek() { + Some('0') => { + value.push('0'); + self.index += 1; + if matches!(self.peek(), Some('0'..='9')) { + return Err(JsonError::new("invalid number: leading zero")); + } + } + Some('1'..='9') => { + while let Some(ch @ '1'..='9') = self.peek() { + value.push(ch); + self.index += 1; + } + while let Some(ch @ '0'..='9') = self.peek() { + value.push(ch); + self.index += 1; + } + } + _ => return Err(JsonError::new("invalid number")), + } + + let mut is_float = false; + if self.try_consume('.') { + is_float = true; + value.push('.'); + let mut has_fraction = false; + while let Some(ch @ '0'..='9') = self.peek() { + value.push(ch); + self.index += 1; + has_fraction = true; + } + if !has_fraction { + return Err(JsonError::new("invalid number: expected digit after '.'")); + } } - if value.is_empty() || value == "-" { - return Err(JsonError::new("invalid number")); + if matches!(self.peek(), Some('e' | 'E')) { + is_float = true; + value.push(self.next().expect("number exponent")); + if matches!(self.peek(), Some('+' | '-')) { + value.push(self.next().expect("number exponent sign")); + } + let exponent_start = self.index; + while let Some(ch @ '0'..='9') = self.peek() { + value.push(ch); + self.index += 1; + } + if self.index == exponent_start { + return Err(JsonError::new("invalid number: expected exponent digit")); + } } - value - .parse::() - .map_err(|_| JsonError::new("number out of range")) + if is_float { + let number = value + .parse::() + .map_err(|_| JsonError::new("number out of range"))?; + if !number.is_finite() { + return Err(JsonError::new("number out of range")); + } + Ok(JsonValue::Float(number)) + } else if let Ok(number) = value.parse::() { + Ok(JsonValue::Number(number)) + } else { + let number = value + .parse::() + .map_err(|_| JsonError::new("number out of range"))?; + if !number.is_finite() { + return Err(JsonError::new("number out of range")); + } + Ok(JsonValue::Float(number)) + } } fn expect(&mut self, expected: char) -> Result<(), JsonError> { @@ -351,6 +424,29 @@ mod tests { assert_eq!(parsed.as_object().expect("object").len(), 2); } + #[test] + fn parses_decimal_and_exponent_numbers() { + for (source, expected) in [ + ("0.1", 0.1_f64), + ("-1.25", -1.25_f64), + ("1e3", 1000.0_f64), + ("2.5E-2", 0.025_f64), + ] { + let parsed = JsonValue::parse(source).expect("number should parse"); + match parsed { + JsonValue::Float(value) => assert!((value - expected).abs() < f64::EPSILON), + other => panic!("expected float, got {other:?}"), + } + } + } + + #[test] + fn rejects_invalid_number_formats() { + for source in ["01", "1.", "1e", "1e+", "-"] { + JsonValue::parse(source).expect_err("invalid number should fail"); + } + } + #[test] fn escapes_control_characters() { assert_eq!(render_string("a\n\t\"b"), "\"a\\n\\t\\\"b\""); diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index f6abca5..0e5a264 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -55,7 +55,7 @@ pub use config::{ McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig, RuntimeHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig, RuntimeTelemetryConfig, - ScopedMcpServerConfig, FCODE_SETTINGS_SCHEMA_NAME, + ScopedMcpServerConfig, FRONTAL_CODE_SETTINGS_SCHEMA_NAME, }; pub use conversation::{ auto_compaction_threshold_from_env, ApiClient, ApiRequest, AssistantEvent, AutoCompactionEvent, diff --git a/crates/runtime/src/oauth.rs b/crates/runtime/src/oauth.rs index 5ad7a04..afedacd 100644 --- a/crates/runtime/src/oauth.rs +++ b/crates/runtime/src/oauth.rs @@ -343,7 +343,7 @@ fn generate_random_token(bytes: usize) -> io::Result { } fn credentials_home_dir() -> io::Result { - if let Some(path) = std::env::var_os("FCODE_CONFIG_HOME") { + if let Some(path) = std::env::var_os("FRONTAL_CODE_CONFIG_HOME") { return Ok(PathBuf::from(path)); } let home = std::env::var_os("HOME") @@ -560,7 +560,7 @@ mod tests { fn oauth_credentials_round_trip_and_clear_preserves_other_fields() { let _guard = env_lock(); let config_home = temp_config_home(); - std::env::set_var("FCODE_CONFIG_HOME", &config_home); + std::env::set_var("FRONTAL_CODE_CONFIG_HOME", &config_home); let path = credentials_path().expect("credentials path"); std::fs::create_dir_all(path.parent().expect("parent")).expect("create parent"); std::fs::write(&path, "{\"other\":\"value\"}\n").expect("seed credentials"); @@ -586,7 +586,7 @@ mod tests { assert!(cleared.contains("\"other\": \"value\"")); assert!(!cleared.contains("\"oauth\"")); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); std::fs::remove_dir_all(config_home).expect("cleanup temp dir"); } diff --git a/crates/runtime/src/prompt.rs b/crates/runtime/src/prompt.rs index 1e9a1c7..cb1a007 100644 --- a/crates/runtime/src/prompt.rs +++ b/crates/runtime/src/prompt.rs @@ -719,9 +719,9 @@ mod tests { ensure_valid_cwd(); let previous = std::env::current_dir().expect("cwd"); let original_home = std::env::var("HOME").ok(); - let original_frontal_code_home = std::env::var("FCODE_CONFIG_HOME").ok(); + let original_frontal_code_home = std::env::var("FRONTAL_CODE_CONFIG_HOME").ok(); std::env::set_var("HOME", &root); - std::env::set_var("FCODE_CONFIG_HOME", root.join("missing-home")); + std::env::set_var("FRONTAL_CODE_CONFIG_HOME", root.join("missing-home")); std::env::set_current_dir(&root).expect("change cwd"); let prompt = super::load_system_prompt(&root, "2026-03-31", "linux", "6.8") .expect("system prompt should load") @@ -737,9 +737,9 @@ mod tests { std::env::remove_var("HOME"); } if let Some(value) = original_frontal_code_home { - std::env::set_var("FCODE_CONFIG_HOME", value); + std::env::set_var("FRONTAL_CODE_CONFIG_HOME", value); } else { - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); } assert!(prompt.contains("Project rules")); diff --git a/crates/sandbox/.env.example b/crates/sandbox/.env.example index f7798fd..0675473 100644 --- a/crates/sandbox/.env.example +++ b/crates/sandbox/.env.example @@ -2,11 +2,11 @@ # Copy this file to .env when validating sandbox behavior locally # Enable Docker fallback backend explicitly (default: disabled) -FCODE_SANDBOX_ENABLE_DOCKER=0 +FRONTAL_CODE_SANDBOX_ENABLE_DOCKER=0 # Docker image used by the sandbox Docker backend when enabled -FCODE_SANDBOX_DOCKER_IMAGE=busybox:1.36 +FRONTAL_CODE_SANDBOX_DOCKER_IMAGE=busybox:1.36 # Optional paths set in sandboxed commands -FCODE_SANDBOX_HOME=~/.frontal-code/sandbox +FRONTAL_CODE_SANDBOX_HOME=~/.frontal-code/sandbox SANDBOX_HOME=~/.frontal-code/sandbox diff --git a/crates/sandbox/BUILD.bazel b/crates/sandbox/BUILD.bazel index 49379d8..fc88c54 100644 --- a/crates/sandbox/BUILD.bazel +++ b/crates/sandbox/BUILD.bazel @@ -16,6 +16,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-sandbox", "test", @@ -28,7 +29,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-sandbox", "fmt-check", @@ -42,6 +45,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-sandbox", "clippy", diff --git a/crates/sandbox/src/lib.rs b/crates/sandbox/src/lib.rs index 2b0c7c2..af6c007 100644 --- a/crates/sandbox/src/lib.rs +++ b/crates/sandbox/src/lib.rs @@ -331,8 +331,8 @@ pub fn build_docker_sandbox_command( return None; } - let image = - env::var("FCODE_SANDBOX_DOCKER_IMAGE").unwrap_or_else(|_| "busybox:1.36".to_string()); + let image = env::var("FRONTAL_CODE_SANDBOX_DOCKER_IMAGE") + .unwrap_or_else(|_| "busybox:1.36".to_string()); let mut args = vec![ "run".to_string(), "--rm".to_string(), @@ -402,11 +402,11 @@ fn sandbox_env(cwd: &Path, status: &SandboxStatus) -> Vec<(String, String)> { ("HOME".to_string(), sandbox_home.display().to_string()), ("TMPDIR".to_string(), sandbox_tmp.display().to_string()), ( - "FCODE_SANDBOX_FILESYSTEM_MODE".to_string(), + "FRONTAL_CODE_SANDBOX_FILESYSTEM_MODE".to_string(), status.filesystem_mode.as_str().to_string(), ), ( - "FCODE_SANDBOX_ALLOWED_MOUNTS".to_string(), + "FRONTAL_CODE_SANDBOX_ALLOWED_MOUNTS".to_string(), status.allowed_mounts.join(":"), ), ]; @@ -449,7 +449,7 @@ fn docker_available() -> bool { } fn docker_backend_enabled() -> bool { - env::var("FCODE_SANDBOX_ENABLE_DOCKER") + env::var("FRONTAL_CODE_SANDBOX_ENABLE_DOCKER") .ok() .is_some_and(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES")) } diff --git a/crates/server/BUILD.bazel b/crates/server/BUILD.bazel index 432ef09..69014e1 100644 --- a/crates/server/BUILD.bazel +++ b/crates/server/BUILD.bazel @@ -20,6 +20,7 @@ sh_binary( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-server", "test", @@ -32,7 +33,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-server", "fmt-check", @@ -46,6 +49,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-server", "clippy", diff --git a/crates/server/README.md b/crates/server/README.md index ca45ccf..e959c84 100644 --- a/crates/server/README.md +++ b/crates/server/README.md @@ -57,12 +57,12 @@ The control plane can apply timed policy to orphaned hosted lanes when a persist Global policy environment variables: -- `FCODE_SERVER_ORPHAN_APPROVAL_DELAY_SECS` -- `FCODE_SERVER_ORPHAN_AUTO_RETRY_SECS` -- `FCODE_SERVER_ORPHAN_AUTO_CANCEL_SECS` -- `FCODE_SERVER_ORPHAN_POLICY_RULES` +- `FRONTAL_CODE_SERVER_ORPHAN_APPROVAL_DELAY_SECS` +- `FRONTAL_CODE_SERVER_ORPHAN_AUTO_RETRY_SECS` +- `FRONTAL_CODE_SERVER_ORPHAN_AUTO_CANCEL_SECS` +- `FRONTAL_CODE_SERVER_ORPHAN_POLICY_RULES` -`FCODE_SERVER_ORPHAN_POLICY_RULES` is a JSON array of ordered match rules. The first matching rule wins. +`FRONTAL_CODE_SERVER_ORPHAN_POLICY_RULES` is a JSON array of ordered match rules. The first matching rule wins. Example: @@ -95,14 +95,14 @@ curl "http://127.0.0.1:8788/v1/policies/orphans?repository=repo-prod&priority=hi The server can run hosted lanes in sibling Docker containers by setting: -- `FCODE_SERVER_LANE_TRANSPORT=local-docker` -- `FCODE_SERVER_WORKSPACE_ROOT=/path/to/server/workspaces` -- `FCODE_SERVER_DOCKER_IMAGE=frontal-code-worker:local` -- `FCODE_SERVER_CALLBACK_URL=http://host.docker.internal:8788` +- `FRONTAL_CODE_SERVER_LANE_TRANSPORT=local-docker` +- `FRONTAL_CODE_SERVER_WORKSPACE_ROOT=/path/to/server/workspaces` +- `FRONTAL_CODE_SERVER_DOCKER_IMAGE=frontal-code-worker:local` +- `FRONTAL_CODE_SERVER_CALLBACK_URL=http://host.docker.internal:8788` In that mode, the control plane: -1. prepares an isolated repo checkout under `FCODE_SERVER_WORKSPACE_ROOT` +1. prepares an isolated repo checkout under `FRONTAL_CODE_SERVER_WORKSPACE_ROOT` 2. writes `.frontal-code-hosted/task.json` into the checkout 3. launches `frontal-code hosted task run TASK_ID` inside the worker image 4. expects the worker container to call `POST /v1/tasks/:task_id/complete` back to the server diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 12d5295..fe827e2 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -169,22 +169,22 @@ impl ServerConfig { pub fn from_env() -> Result> { let mut config = Self::default(); - if let Ok(host) = env::var("FCODE_SERVER_HOST") { + if let Ok(host) = env::var("FRONTAL_CODE_SERVER_HOST") { let ip: IpAddr = host.parse()?; config.bind_addr = SocketAddr::new(ip, config.bind_addr.port()); } - if let Ok(port) = env::var("FCODE_SERVER_PORT") { + if let Ok(port) = env::var("FRONTAL_CODE_SERVER_PORT") { let port: u16 = port.parse()?; config.bind_addr = SocketAddr::new(config.bind_addr.ip(), port); } - if let Ok(limit) = env::var("FCODE_SERVER_EVENT_REPLAY_LIMIT") { + if let Ok(limit) = env::var("FRONTAL_CODE_SERVER_EVENT_REPLAY_LIMIT") { let limit: usize = limit.parse()?; config.event_replay_limit = limit.max(1); } - if let Ok(kind) = env::var("FCODE_SERVER_LANE_TRANSPORT") { + if let Ok(kind) = env::var("FRONTAL_CODE_SERVER_LANE_TRANSPORT") { config.lane_transport_kind = match kind.trim().to_ascii_lowercase().as_str() { "docker" | "local-docker" | "local_docker" => LaneTransportKind::LocalDocker, "tools-agent" | "tools_agent" | "agent" => LaneTransportKind::ToolsAgent, @@ -192,64 +192,64 @@ impl ServerConfig { }; } - if let Ok(api_key) = env::var("FCODE_SERVER_API_KEY") { + if let Ok(api_key) = env::var("FRONTAL_CODE_SERVER_API_KEY") { let api_key = api_key.trim(); if !api_key.is_empty() { config.api_key = Some(api_key.to_string()); } } - if let Ok(path) = env::var("FCODE_SERVER_WORKSPACE_ROOT") { + if let Ok(path) = env::var("FRONTAL_CODE_SERVER_WORKSPACE_ROOT") { let path = path.trim(); if !path.is_empty() { config.workspace_root = PathBuf::from(path); } } - if let Ok(image) = env::var("FCODE_SERVER_DOCKER_IMAGE") { + if let Ok(image) = env::var("FRONTAL_CODE_SERVER_DOCKER_IMAGE") { let image = image.trim(); if !image.is_empty() { config.docker_image = image.to_string(); } } - if let Ok(url) = env::var("FCODE_SERVER_CALLBACK_URL") { + if let Ok(url) = env::var("FRONTAL_CODE_SERVER_CALLBACK_URL") { let url = url.trim().trim_end_matches('/'); if !url.is_empty() { config.docker_server_url = url.to_string(); } } - if let Ok(interval) = env::var("FCODE_SERVER_RECONCILE_INTERVAL_SECS") { + if let Ok(interval) = env::var("FRONTAL_CODE_SERVER_RECONCILE_INTERVAL_SECS") { let interval: u64 = interval.parse()?; config.reconcile_interval = (interval > 0).then(|| Duration::from_secs(interval)); } - if let Ok(path) = env::var("FCODE_SERVER_STATE_FILE") { + if let Ok(path) = env::var("FRONTAL_CODE_SERVER_STATE_FILE") { let path = path.trim(); config.state_file = (!path.is_empty()).then(|| PathBuf::from(path)); } - if let Ok(delay) = env::var("FCODE_SERVER_ORPHAN_APPROVAL_DELAY_SECS") { + if let Ok(delay) = env::var("FRONTAL_CODE_SERVER_ORPHAN_APPROVAL_DELAY_SECS") { let delay: u64 = delay.parse()?; config.orphan_approval_delay = Duration::from_secs(delay); } - if let Ok(delay) = env::var("FCODE_SERVER_ORPHAN_AUTO_RETRY_SECS") { + if let Ok(delay) = env::var("FRONTAL_CODE_SERVER_ORPHAN_AUTO_RETRY_SECS") { let delay: u64 = delay.parse()?; config.orphan_auto_retry_after = (delay > 0).then(|| Duration::from_secs(delay)); } - if let Ok(delay) = env::var("FCODE_SERVER_ORPHAN_AUTO_CANCEL_SECS") { + if let Ok(delay) = env::var("FRONTAL_CODE_SERVER_ORPHAN_AUTO_CANCEL_SECS") { let delay: u64 = delay.parse()?; config.orphan_auto_cancel_after = (delay > 0).then(|| Duration::from_secs(delay)); } - if let Ok(rules) = env::var("FCODE_SERVER_ORPHAN_POLICY_RULES") { + if let Ok(rules) = env::var("FRONTAL_CODE_SERVER_ORPHAN_POLICY_RULES") { let rules = rules.trim(); if !rules.is_empty() { config.orphan_policy_rules = serde_json::from_str(rules).map_err(|error| { - format!("invalid FCODE_SERVER_ORPHAN_POLICY_RULES: {error}") + format!("invalid FRONTAL_CODE_SERVER_ORPHAN_POLICY_RULES: {error}") })?; } } @@ -1579,7 +1579,7 @@ pub fn app(state: Arc) -> Router { /// Environment variable that lets an operator deliberately run the control /// plane with no API key (local development, or a trusted private network). -const ALLOW_ANONYMOUS_ENV: &str = "FCODE_SERVER_ALLOW_ANONYMOUS"; +const ALLOW_ANONYMOUS_ENV: &str = "FRONTAL_CODE_SERVER_ALLOW_ANONYMOUS"; fn is_truthy(value: &str) -> bool { matches!( @@ -1602,16 +1602,16 @@ fn check_auth_posture(config: &ServerConfig) -> Result<(), String> { let allowed = env::var(ALLOW_ANONYMOUS_ENV).is_ok_and(|value| is_truthy(&value)); if !allowed { return Err(format!( - "refusing to start: FCODE_SERVER_API_KEY is not set, so every control-plane \ + "refusing to start: FRONTAL_CODE_SERVER_API_KEY is not set, so every control-plane \ route (task create/cancel/complete/approval, connector events, the event \ stream) would accept unauthenticated requests.\n\ - Set FCODE_SERVER_API_KEY to a secret, or set {ALLOW_ANONYMOUS_ENV}=1 to \ + Set FRONTAL_CODE_SERVER_API_KEY to a secret, or set {ALLOW_ANONYMOUS_ENV}=1 to \ accept an open control plane on a trusted network." )); } eprintln!( - "warning: {ALLOW_ANONYMOUS_ENV} is set and FCODE_SERVER_API_KEY is not — the \ + "warning: {ALLOW_ANONYMOUS_ENV} is set and FRONTAL_CODE_SERVER_API_KEY is not — the \ control plane will accept unauthenticated requests on {}", config.bind_addr ); @@ -1647,10 +1647,12 @@ pub async fn serve(config: ServerConfig) -> Result<(), Box(&raw) { if let Err(err) = register_integrations_from_json(&value) { - eprintln!("failed to register integrations from FCODE_INTEGRATIONS_CONFIG: {err}"); + eprintln!( + "failed to register integrations from FRONTAL_CODE_INTEGRATIONS_CONFIG: {err}" + ); } } } @@ -1680,7 +1682,7 @@ async fn generic_oauth_authorize( let client_id = std::env::var(&oauth.client_id_env) .map_err(|_| AppError::internal(format!("{} not set", oauth.client_id_env)))?; - let redirect_uri = std::env::var("FCODE_OAUTH_REDIRECT_BASE").map_or_else( + let redirect_uri = std::env::var("FRONTAL_CODE_OAUTH_REDIRECT_BASE").map_or_else( |_| format!("https://frontal-code.fly.dev/v1/oauth/{provider}/callback"), |base| format!("{base}/v1/oauth/{provider}/callback"), ); @@ -2958,7 +2960,7 @@ fn verify_hmac_signature(secret: &str, signature_header: Option<&str>, body: &[u mac.verify_slice(&expected).is_ok() } -/// Webhook sources map onto `FCODE__WEBHOOK_SECRET`, so restrict them to +/// Webhook sources map onto `FRONTAL_CODE__WEBHOOK_SECRET`, so restrict them to /// a shape that can only name a variable an operator deliberately created. fn is_valid_webhook_source(source: &str) -> bool { !source.is_empty() @@ -2983,7 +2985,7 @@ async fn integration_webhook( // This route sits outside the control-plane auth layer, so the signature is // the only thing standing between an anonymous POST and task-state // mutation. An unconfigured secret is a rejection, never a bypass. - let secret_env = format!("FCODE_{}_WEBHOOK_SECRET", source.to_uppercase()); + let secret_env = format!("FRONTAL_CODE_{}_WEBHOOK_SECRET", source.to_uppercase()); let Ok(secret) = env::var(&secret_env) else { return StatusCode::UNAUTHORIZED; }; @@ -3547,20 +3549,23 @@ impl LaneWorkerTransport for LocalDockerLaneWorkerTransport { .map_err(|error| format!("failed to write hosted docker task payload: {error}"))?; let image = self.image.clone(); let mut container_env = BTreeMap::from([ - ("FCODE_SERVER_URL".to_string(), self.server_url.clone()), ( - "FCODE_HOSTED_TASK_FILE".to_string(), + "FRONTAL_CODE_SERVER_URL".to_string(), + self.server_url.clone(), + ), + ( + "FRONTAL_CODE_HOSTED_TASK_FILE".to_string(), "/workspace/.frontal-code-hosted/task.json".to_string(), ), ]); if let Some(api_key) = &self.server_api_key { - container_env.insert("FCODE_SERVER_API_KEY".to_string(), api_key.clone()); + container_env.insert("FRONTAL_CODE_SERVER_API_KEY".to_string(), api_key.clone()); } for key in [ "GITHUB_TOKEN", - "FCODE_GITHUB_API_BASE", - "FCODE_GIT_AUTHOR_NAME", - "FCODE_GIT_AUTHOR_EMAIL", + "FRONTAL_CODE_GITHUB_API_BASE", + "FRONTAL_CODE_GIT_AUTHOR_NAME", + "FRONTAL_CODE_GIT_AUTHOR_EMAIL", ] { if let Ok(value) = env::var(key) { if !value.trim().is_empty() { @@ -4966,20 +4971,23 @@ mod tests { ] ); assert_eq!( - launched[0].env.get("FCODE_SERVER_URL").map(String::as_str), + launched[0] + .env + .get("FRONTAL_CODE_SERVER_URL") + .map(String::as_str), Some("http://host.docker.internal:8788") ); assert_eq!( launched[0] .env - .get("FCODE_HOSTED_TASK_FILE") + .get("FRONTAL_CODE_HOSTED_TASK_FILE") .map(String::as_str), Some("/workspace/.frontal-code-hosted/task.json") ); assert_eq!( launched[0] .env - .get("FCODE_SERVER_API_KEY") + .get("FRONTAL_CODE_SERVER_API_KEY") .map(String::as_str), Some("server-secret") ); @@ -5508,9 +5516,9 @@ mod tests { let state = Arc::new(ServerState::with_lane_transport_and_policy_rules( DEFAULT_EVENT_REPLAY_LIMIT, Arc::new(InMemoryLaneWorkerTransport::default()), - Duration::from_secs(120), - Some(Duration::from_secs(60)), - Some(Duration::from_secs(600)), + Duration::from_mins(2), + Some(Duration::from_mins(1)), + Some(Duration::from_mins(10)), vec![OrphanPolicyRule { repository: Some("repo-ops".to_string()), source: Some("slack".to_string()), @@ -5551,7 +5559,7 @@ mod tests { let state = Arc::new(ServerState::with_lane_transport_and_policy_rules( DEFAULT_EVENT_REPLAY_LIMIT, Arc::new(InMemoryLaneWorkerTransport::default()), - Duration::from_secs(300), + Duration::from_mins(5), None, None, vec![OrphanPolicyRule { @@ -6396,7 +6404,7 @@ mod tests { manifest_file: manifest_file.display().to_string(), output_file: output_file.display().to_string(), }), - Duration::from_secs(300), + Duration::from_mins(5), None, None, )); @@ -6513,9 +6521,9 @@ mod tests { manifest_file: manifest_file.display().to_string(), output_file: output_file.display().to_string(), }), - Duration::from_secs(300), + Duration::from_mins(5), None, - Some(Duration::from_secs(60)), + Some(Duration::from_mins(1)), )); let router = app(state.clone()); @@ -6609,8 +6617,8 @@ mod tests { manifest_file: manifest_file.display().to_string(), output_file: output_file.display().to_string(), }), - Duration::from_secs(300), - Some(Duration::from_secs(60)), + Duration::from_mins(5), + Some(Duration::from_mins(1)), None, )); let router = app(state.clone()); @@ -6706,8 +6714,8 @@ mod tests { manifest_file: manifest_file.display().to_string(), output_file: output_file.display().to_string(), }), - Duration::from_secs(120), - Some(Duration::from_secs(60)), + Duration::from_mins(2), + Some(Duration::from_mins(1)), None, )); let router = app(state.clone()); @@ -6791,7 +6799,7 @@ mod tests { manifest_file: manifest_file.display().to_string(), output_file: output_file.display().to_string(), }), - Duration::from_secs(300), + Duration::from_mins(5), None, None, vec![OrphanPolicyRule { @@ -7250,7 +7258,10 @@ mod tests { #[allow(clippy::await_holding_lock, clippy::too_many_lines)] async fn github_webhook_route_correlates_pull_request_event_to_hosted_task() { let _lock = GITHUB_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_GITHUB_WEBHOOK_SECRET", Some(TEST_WEBHOOK_SECRET)); + let _secret = EnvVarGuard::set( + "FRONTAL_CODE_GITHUB_WEBHOOK_SECRET", + Some(TEST_WEBHOOK_SECRET), + ); let state = Arc::new(ServerState::default()); let router = app(state.clone()); @@ -7374,7 +7385,10 @@ mod tests { #[allow(clippy::await_holding_lock, clippy::too_many_lines)] async fn github_webhook_route_persists_closed_merge_state_for_hosted_task() { let _lock = GITHUB_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_GITHUB_WEBHOOK_SECRET", Some(TEST_WEBHOOK_SECRET)); + let _secret = EnvVarGuard::set( + "FRONTAL_CODE_GITHUB_WEBHOOK_SECRET", + Some(TEST_WEBHOOK_SECRET), + ); let state = Arc::new(ServerState::default()); let router = app(state.clone()); @@ -7485,7 +7499,10 @@ mod tests { #[allow(clippy::await_holding_lock)] async fn github_review_webhook_requests_followup_for_hosted_task() { let _lock = GITHUB_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_GITHUB_WEBHOOK_SECRET", Some(TEST_WEBHOOK_SECRET)); + let _secret = EnvVarGuard::set( + "FRONTAL_CODE_GITHUB_WEBHOOK_SECRET", + Some(TEST_WEBHOOK_SECRET), + ); let state = Arc::new(ServerState::default()); let router = app(state.clone()); @@ -7587,7 +7604,10 @@ mod tests { #[allow(clippy::await_holding_lock, clippy::too_many_lines)] async fn github_review_approval_webhook_clears_followup_for_hosted_task() { let _lock = GITHUB_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_GITHUB_WEBHOOK_SECRET", Some(TEST_WEBHOOK_SECRET)); + let _secret = EnvVarGuard::set( + "FRONTAL_CODE_GITHUB_WEBHOOK_SECRET", + Some(TEST_WEBHOOK_SECRET), + ); let state = Arc::new(ServerState::default()); let router = app(state.clone()); @@ -7701,7 +7721,10 @@ mod tests { #[allow(clippy::await_holding_lock)] async fn github_webhook_route_emits_unmatched_event_without_task_binding() { let _lock = GITHUB_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_GITHUB_WEBHOOK_SECRET", Some(TEST_WEBHOOK_SECRET)); + let _secret = EnvVarGuard::set( + "FRONTAL_CODE_GITHUB_WEBHOOK_SECRET", + Some(TEST_WEBHOOK_SECRET), + ); let state = Arc::new(ServerState::default()); let router = app(state.clone()); @@ -7760,7 +7783,7 @@ mod tests { #[allow(clippy::await_holding_lock)] async fn linear_webhook_rejects_invalid_signature_when_secret_set() { let _lock = CONNECTOR_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_LINEAR_WEBHOOK_SECRET", Some("secret")); + let _secret = EnvVarGuard::set("FRONTAL_CODE_LINEAR_WEBHOOK_SECRET", Some("secret")); let state = Arc::new( ServerState::new_with_transport_kind_state_file_policy_and_workspace_root( 10, @@ -7802,7 +7825,7 @@ mod tests { #[allow(clippy::await_holding_lock)] async fn graphite_webhook_rejects_invalid_signature_when_secret_set() { let _lock = CONNECTOR_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_GRAPHITE_WEBHOOK_SECRET", Some("secret")); + let _secret = EnvVarGuard::set("FRONTAL_CODE_GRAPHITE_WEBHOOK_SECRET", Some("secret")); let state = Arc::new( ServerState::new_with_transport_kind_state_file_policy_and_workspace_root( 10, @@ -7844,7 +7867,10 @@ mod tests { #[allow(clippy::await_holding_lock)] async fn linear_webhook_matches_task_and_updates_context() { let _lock = CONNECTOR_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_LINEAR_WEBHOOK_SECRET", Some(TEST_WEBHOOK_SECRET)); + let _secret = EnvVarGuard::set( + "FRONTAL_CODE_LINEAR_WEBHOOK_SECRET", + Some(TEST_WEBHOOK_SECRET), + ); let state = Arc::new(ServerState::default()); let router = app(state.clone()); @@ -7916,7 +7942,10 @@ mod tests { #[allow(clippy::await_holding_lock)] async fn linear_webhook_no_match_returns_accepted() { let _lock = CONNECTOR_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_LINEAR_WEBHOOK_SECRET", Some(TEST_WEBHOOK_SECRET)); + let _secret = EnvVarGuard::set( + "FRONTAL_CODE_LINEAR_WEBHOOK_SECRET", + Some(TEST_WEBHOOK_SECRET), + ); let state = Arc::new(ServerState::default()); let router = app(state.clone()); @@ -7938,7 +7967,10 @@ mod tests { #[allow(clippy::await_holding_lock)] async fn graphite_webhook_matches_task_and_updates_context() { let _lock = CONNECTOR_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_GRAPHITE_WEBHOOK_SECRET", Some(TEST_WEBHOOK_SECRET)); + let _secret = EnvVarGuard::set( + "FRONTAL_CODE_GRAPHITE_WEBHOOK_SECRET", + Some(TEST_WEBHOOK_SECRET), + ); let state = Arc::new(ServerState::default()); let router = app(state.clone()); @@ -8006,7 +8038,10 @@ mod tests { #[allow(clippy::await_holding_lock)] async fn graphite_webhook_no_match_returns_accepted() { let _lock = CONNECTOR_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_GRAPHITE_WEBHOOK_SECRET", Some(TEST_WEBHOOK_SECRET)); + let _secret = EnvVarGuard::set( + "FRONTAL_CODE_GRAPHITE_WEBHOOK_SECRET", + Some(TEST_WEBHOOK_SECRET), + ); let state = Arc::new(ServerState::default()); let router = app(state.clone()); @@ -8028,7 +8063,7 @@ mod tests { #[allow(clippy::await_holding_lock)] async fn github_webhook_rejects_invalid_signature_when_secret_set() { let _lock = GITHUB_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_GITHUB_WEBHOOK_SECRET", Some("secret")); + let _secret = EnvVarGuard::set("FRONTAL_CODE_GITHUB_WEBHOOK_SECRET", Some("secret")); let state = Arc::new(ServerState::new(10)); let payload = json!({ @@ -8058,7 +8093,7 @@ mod tests { #[allow(clippy::await_holding_lock)] async fn github_webhook_accepts_valid_x_hub_signature_256() { let _lock = GITHUB_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_GITHUB_WEBHOOK_SECRET", Some("secret")); + let _secret = EnvVarGuard::set("FRONTAL_CODE_GITHUB_WEBHOOK_SECRET", Some("secret")); let state = Arc::new(ServerState::new(10)); let payload = json!({ @@ -8121,7 +8156,7 @@ mod tests { #[allow(clippy::await_holding_lock)] async fn webhook_rejects_delivery_when_no_secret_is_configured() { let _lock = GITHUB_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_GITHUB_WEBHOOK_SECRET", None); + let _secret = EnvVarGuard::set("FRONTAL_CODE_GITHUB_WEBHOOK_SECRET", None); let router = app(Arc::new(ServerState::default())); let response = router @@ -8142,7 +8177,7 @@ mod tests { #[allow(clippy::await_holding_lock)] async fn webhook_rejects_delivery_when_secret_is_blank() { let _lock = GITHUB_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_GITHUB_WEBHOOK_SECRET", Some(" ")); + let _secret = EnvVarGuard::set("FRONTAL_CODE_GITHUB_WEBHOOK_SECRET", Some(" ")); let router = app(Arc::new(ServerState::default())); let response = router @@ -8177,7 +8212,7 @@ mod tests { assert!(config.api_key.is_none()); let error = check_auth_posture(&config).expect_err("missing API key should be refused"); - assert!(error.contains("FCODE_SERVER_API_KEY")); + assert!(error.contains("FRONTAL_CODE_SERVER_API_KEY")); } #[test] @@ -8204,7 +8239,10 @@ mod tests { #[allow(clippy::await_holding_lock)] async fn webhook_routes_remain_public_when_control_plane_api_key_is_configured() { let _lock = GITHUB_WEBHOOK_ENV_LOCK.lock().unwrap(); - let _secret = EnvVarGuard::set("FCODE_GITHUB_WEBHOOK_SECRET", Some(TEST_WEBHOOK_SECRET)); + let _secret = EnvVarGuard::set( + "FRONTAL_CODE_GITHUB_WEBHOOK_SECRET", + Some(TEST_WEBHOOK_SECRET), + ); let state = Arc::new( ServerState::new(10).with_control_plane_api_key(Some("top-secret".to_string())), ); @@ -9279,18 +9317,21 @@ mod tests { #[test] fn server_config_parses_local_docker_lane_transport_kind() { - let previous = env::var_os("FCODE_SERVER_LANE_TRANSPORT"); - let previous_workspace_root = env::var_os("FCODE_SERVER_WORKSPACE_ROOT"); - let previous_docker_image = env::var_os("FCODE_SERVER_DOCKER_IMAGE"); - let previous_callback_url = env::var_os("FCODE_SERVER_CALLBACK_URL"); - env::set_var("FCODE_SERVER_LANE_TRANSPORT", "local-docker"); + let previous = env::var_os("FRONTAL_CODE_SERVER_LANE_TRANSPORT"); + let previous_workspace_root = env::var_os("FRONTAL_CODE_SERVER_WORKSPACE_ROOT"); + let previous_docker_image = env::var_os("FRONTAL_CODE_SERVER_DOCKER_IMAGE"); + let previous_callback_url = env::var_os("FRONTAL_CODE_SERVER_CALLBACK_URL"); + env::set_var("FRONTAL_CODE_SERVER_LANE_TRANSPORT", "local-docker"); env::set_var( - "FCODE_SERVER_WORKSPACE_ROOT", + "FRONTAL_CODE_SERVER_WORKSPACE_ROOT", "/tmp/frontal-code-server-workspaces", ); - env::set_var("FCODE_SERVER_DOCKER_IMAGE", "frontal-code-worker:test"); env::set_var( - "FCODE_SERVER_CALLBACK_URL", + "FRONTAL_CODE_SERVER_DOCKER_IMAGE", + "frontal-code-worker:test", + ); + env::set_var( + "FRONTAL_CODE_SERVER_CALLBACK_URL", "http://docker.internal.test:8788/", ); @@ -9304,20 +9345,20 @@ mod tests { assert_eq!(config.docker_server_url, "http://docker.internal.test:8788"); match previous { - Some(value) => env::set_var("FCODE_SERVER_LANE_TRANSPORT", value), - None => env::remove_var("FCODE_SERVER_LANE_TRANSPORT"), + Some(value) => env::set_var("FRONTAL_CODE_SERVER_LANE_TRANSPORT", value), + None => env::remove_var("FRONTAL_CODE_SERVER_LANE_TRANSPORT"), } match previous_workspace_root { - Some(value) => env::set_var("FCODE_SERVER_WORKSPACE_ROOT", value), - None => env::remove_var("FCODE_SERVER_WORKSPACE_ROOT"), + Some(value) => env::set_var("FRONTAL_CODE_SERVER_WORKSPACE_ROOT", value), + None => env::remove_var("FRONTAL_CODE_SERVER_WORKSPACE_ROOT"), } match previous_docker_image { - Some(value) => env::set_var("FCODE_SERVER_DOCKER_IMAGE", value), - None => env::remove_var("FCODE_SERVER_DOCKER_IMAGE"), + Some(value) => env::set_var("FRONTAL_CODE_SERVER_DOCKER_IMAGE", value), + None => env::remove_var("FRONTAL_CODE_SERVER_DOCKER_IMAGE"), } match previous_callback_url { - Some(value) => env::set_var("FCODE_SERVER_CALLBACK_URL", value), - None => env::remove_var("FCODE_SERVER_CALLBACK_URL"), + Some(value) => env::set_var("FRONTAL_CODE_SERVER_CALLBACK_URL", value), + None => env::remove_var("FRONTAL_CODE_SERVER_CALLBACK_URL"), } } diff --git a/crates/telemetry/BUILD.bazel b/crates/telemetry/BUILD.bazel index 5453cee..ca6b990 100644 --- a/crates/telemetry/BUILD.bazel +++ b/crates/telemetry/BUILD.bazel @@ -11,6 +11,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-telemetry", "test", @@ -23,7 +24,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-telemetry", "fmt-check", @@ -37,6 +40,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-telemetry", "clippy", diff --git a/crates/telemetry/src/lib.rs b/crates/telemetry/src/lib.rs index 9c020a6..e01edd4 100644 --- a/crates/telemetry/src/lib.rs +++ b/crates/telemetry/src/lib.rs @@ -9,7 +9,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -pub const DEFAULT_FCODE_VERSION: &str = "2023-06-01"; +pub const DEFAULT_FRONTAL_CODE_VERSION: &str = "2023-06-01"; pub const DEFAULT_APP_NAME: &str = "claude-code"; pub const DEFAULT_RUNTIME: &str = "rust"; pub const DEFAULT_AGENTIC_BETA: &str = "claude-code-20250219"; @@ -64,7 +64,7 @@ impl AnthropicRequestProfile { #[must_use] pub fn new(client_identity: ClientIdentity) -> Self { Self { - anthropic_version: DEFAULT_FCODE_VERSION.to_string(), + anthropic_version: DEFAULT_FRONTAL_CODE_VERSION.to_string(), client_identity, betas: vec![ DEFAULT_AGENTIC_BETA.to_string(), @@ -444,7 +444,7 @@ mod tests { vec![ ( "anthropic-version".to_string(), - DEFAULT_FCODE_VERSION.to_string() + DEFAULT_FRONTAL_CODE_VERSION.to_string() ), ("user-agent".to_string(), "claude-code/1.2.3".to_string()), ( diff --git a/crates/telemetry/tests/request_profile.rs b/crates/telemetry/tests/request_profile.rs index f61e000..592e262 100644 --- a/crates/telemetry/tests/request_profile.rs +++ b/crates/telemetry/tests/request_profile.rs @@ -1,5 +1,5 @@ use frontal_code_telemetry::{ - AnthropicRequestProfile, ClientIdentity, DEFAULT_AGENTIC_BETA, DEFAULT_FCODE_VERSION, + AnthropicRequestProfile, ClientIdentity, DEFAULT_AGENTIC_BETA, DEFAULT_FRONTAL_CODE_VERSION, DEFAULT_PROMPT_CACHING_SCOPE_BETA, }; use serde_json::{json, Map}; @@ -8,7 +8,7 @@ use serde_json::{json, Map}; fn new_creates_profile_with_default_betas() { let identity = ClientIdentity::new("app", "1.0"); let profile = AnthropicRequestProfile::new(identity); - assert_eq!(profile.anthropic_version, DEFAULT_FCODE_VERSION); + assert_eq!(profile.anthropic_version, DEFAULT_FRONTAL_CODE_VERSION); assert_eq!(profile.betas.len(), 2); assert!(profile.betas.contains(&DEFAULT_AGENTIC_BETA.to_string())); assert!(profile @@ -53,7 +53,7 @@ fn header_pairs_includes_version_and_user_agent() { let headers = profile.header_pairs(); assert!(headers.contains(&( "anthropic-version".to_string(), - DEFAULT_FCODE_VERSION.to_string() + DEFAULT_FRONTAL_CODE_VERSION.to_string() ))); assert!(headers.contains(&("user-agent".to_string(), "cli/2.0".to_string()))); } @@ -75,7 +75,7 @@ fn header_pairs_includes_beta_header_when_betas_present() { #[test] fn header_pairs_empty_betas_omits_beta_header() { let profile = AnthropicRequestProfile { - anthropic_version: DEFAULT_FCODE_VERSION.to_string(), + anthropic_version: DEFAULT_FRONTAL_CODE_VERSION.to_string(), client_identity: ClientIdentity::default(), betas: vec![], extra_body: Map::default(), @@ -98,7 +98,7 @@ fn render_json_body_merges_betas_and_extra_body() { #[test] fn render_json_body_without_betas() { let profile = AnthropicRequestProfile { - anthropic_version: DEFAULT_FCODE_VERSION.to_string(), + anthropic_version: DEFAULT_FRONTAL_CODE_VERSION.to_string(), client_identity: ClientIdentity::default(), betas: vec![], extra_body: Map::default(), @@ -120,7 +120,7 @@ fn render_json_body_fails_on_non_object() { fn default_uses_default_client_identity() { let profile = AnthropicRequestProfile::default(); assert_eq!(profile.client_identity, ClientIdentity::default()); - assert_eq!(profile.anthropic_version, DEFAULT_FCODE_VERSION); + assert_eq!(profile.anthropic_version, DEFAULT_FRONTAL_CODE_VERSION); } #[test] diff --git a/crates/tools/BUILD.bazel b/crates/tools/BUILD.bazel index 6c17e38..dcfc649 100644 --- a/crates/tools/BUILD.bazel +++ b/crates/tools/BUILD.bazel @@ -11,6 +11,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-tools", "test", @@ -23,7 +24,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-tools", "fmt-check", @@ -37,6 +40,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-tools", "clippy", diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index a0c74e9..4467c90 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -40,7 +40,7 @@ use reqwest::blocking::Client; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; -const FCODE_TOOL_TELEMETRY_PATH: &str = "FCODE_TOOL_TELEMETRY_PATH"; +const FRONTAL_CODE_TOOL_TELEMETRY_PATH: &str = "FRONTAL_CODE_TOOL_TELEMETRY_PATH"; /// Global task registry shared across tool invocations within a session. fn global_lsp_registry() -> &'static LspRegistry { @@ -196,7 +196,7 @@ struct ToolTelemetry { impl ToolTelemetry { #[must_use] fn from_env() -> Self { - env::var(FCODE_TOOL_TELEMETRY_PATH) + env::var(FRONTAL_CODE_TOOL_TELEMETRY_PATH) .ok() .map(|path| path.trim().to_string()) .filter(|path| !path.is_empty()) @@ -3410,7 +3410,7 @@ fn normalize_fetch_url(url: &str) -> Result { } fn build_search_url(query: &str) -> Result { - if let Ok(base) = std::env::var("FCODE_WEB_SEARCH_BASE_URL") { + if let Ok(base) = std::env::var("FRONTAL_CODE_WEB_SEARCH_BASE_URL") { let mut url = reqwest::Url::parse(&base).map_err(|error| error.to_string())?; url.query_pairs_mut().append_pair("q", query); return Ok(url); @@ -3755,7 +3755,7 @@ fn validate_todos(todos: &[TodoItem]) -> Result<(), String> { } fn todo_store_path() -> Result { - if let Ok(path) = std::env::var("FCODE_TODO_STORE") { + if let Ok(path) = std::env::var("FRONTAL_CODE_TODO_STORE") { return Ok(std::path::PathBuf::from(path)); } let cwd = std::env::current_dir().map_err(|error| error.to_string())?; @@ -3804,7 +3804,7 @@ fn skill_lookup_roots() -> Vec { push_project_skill_lookup_roots(&mut roots, &cwd); } - if let Ok(frontal_code_config_home) = std::env::var("FCODE_CONFIG_HOME") { + if let Ok(frontal_code_config_home) = std::env::var("FRONTAL_CODE_CONFIG_HOME") { push_prefixed_skill_lookup_roots( &mut roots, std::path::Path::new(&frontal_code_config_home), @@ -4686,15 +4686,15 @@ fn maybe_report_hosted_task_completion( } fn read_hosted_server_url() -> Option { - env::var("FCODE_SERVER_URL") + env::var("FRONTAL_CODE_SERVER_URL") .ok() - .or_else(|| env::var("FCODE_SERVER_BASE_URL").ok()) + .or_else(|| env::var("FRONTAL_CODE_SERVER_BASE_URL").ok()) .map(|value| value.trim().trim_end_matches('/').to_string()) .filter(|value| !value.is_empty()) } fn read_hosted_server_api_key() -> Option { - env::var("FCODE_SERVER_API_KEY") + env::var("FRONTAL_CODE_SERVER_API_KEY") .ok() .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) @@ -5355,7 +5355,7 @@ fn canonical_tool_token(value: &str) -> String { } fn agent_store_dir() -> Result { - if let Ok(path) = std::env::var("FCODE_AGENT_STORE") { + if let Ok(path) = std::env::var("FRONTAL_CODE_AGENT_STORE") { return Ok(std::path::PathBuf::from(path)); } let cwd = std::env::current_dir().map_err(|error| error.to_string())?; @@ -6103,7 +6103,7 @@ fn config_file_for_scope(scope: ConfigScope) -> Result { } fn config_home_dir() -> Result { - if let Ok(path) = std::env::var("FCODE_CONFIG_HOME") { + if let Ok(path) = std::env::var("FRONTAL_CODE_CONFIG_HOME") { return Ok(PathBuf::from(path)); } let home = std::env::var("HOME").map_err(|_| String::from("HOME is not set"))?; @@ -7070,7 +7070,7 @@ mod tests { })); std::env::set_var( - "FCODE_WEB_SEARCH_BASE_URL", + "FRONTAL_CODE_WEB_SEARCH_BASE_URL", format!("http://{}/search", server.addr()), ); let result = execute_tool( @@ -7082,7 +7082,7 @@ mod tests { }), ) .expect("WebSearch should succeed"); - std::env::remove_var("FCODE_WEB_SEARCH_BASE_URL"); + std::env::remove_var("FRONTAL_CODE_WEB_SEARCH_BASE_URL"); let output: serde_json::Value = serde_json::from_str(&result).expect("valid json"); assert_eq!(output["query"], "rust web search"); @@ -7123,7 +7123,7 @@ mod tests { })); std::env::set_var( - "FCODE_WEB_SEARCH_BASE_URL", + "FRONTAL_CODE_WEB_SEARCH_BASE_URL", format!("http://{}/fallback", server.addr()), ); let result = execute_tool( @@ -7133,7 +7133,7 @@ mod tests { }), ) .expect("WebSearch fallback parsing should succeed"); - std::env::remove_var("FCODE_WEB_SEARCH_BASE_URL"); + std::env::remove_var("FRONTAL_CODE_WEB_SEARCH_BASE_URL"); let output: serde_json::Value = serde_json::from_str(&result).expect("valid json"); let results = output["results"].as_array().expect("results array"); @@ -7146,10 +7146,10 @@ mod tests { assert_eq!(content[0]["url"], "https://example.com/one"); assert_eq!(content[1]["url"], "https://docs.rs/tokio"); - std::env::set_var("FCODE_WEB_SEARCH_BASE_URL", "://bad-base-url"); + std::env::set_var("FRONTAL_CODE_WEB_SEARCH_BASE_URL", "://bad-base-url"); let error = execute_tool("WebSearch", &json!({ "query": "generic links" })) .expect_err("invalid base URL should fail"); - std::env::remove_var("FCODE_WEB_SEARCH_BASE_URL"); + std::env::remove_var("FRONTAL_CODE_WEB_SEARCH_BASE_URL"); assert!(error.contains("relative URL without a base") || error.contains("empty host")); } @@ -7216,7 +7216,7 @@ mod tests { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let path = temp_path("todos.json"); - std::env::set_var("FCODE_TODO_STORE", &path); + std::env::set_var("FRONTAL_CODE_TODO_STORE", &path); let first = execute_tool( "TodoWrite", @@ -7242,7 +7242,7 @@ mod tests { }), ) .expect("TodoWrite should succeed"); - std::env::remove_var("FCODE_TODO_STORE"); + std::env::remove_var("FRONTAL_CODE_TODO_STORE"); let _ = std::fs::remove_file(path); let second_output: serde_json::Value = serde_json::from_str(&second).expect("valid json"); @@ -7263,7 +7263,7 @@ mod tests { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let path = temp_path("todos-errors.json"); - std::env::set_var("FCODE_TODO_STORE", &path); + std::env::set_var("FRONTAL_CODE_TODO_STORE", &path); let empty = execute_tool("TodoWrite", &json!({ "todos": [] })) .expect_err("empty todos should fail"); @@ -7303,7 +7303,7 @@ mod tests { }), ) .expect("completed todos should succeed"); - std::env::remove_var("FCODE_TODO_STORE"); + std::env::remove_var("FRONTAL_CODE_TODO_STORE"); let _ = fs::remove_file(path); let output: serde_json::Value = serde_json::from_str(&nudge).expect("valid json"); @@ -7431,11 +7431,11 @@ mod tests { .expect("skill file should exist"); let original_home = std::env::var("HOME").ok(); - let original_config_home = std::env::var("FCODE_CONFIG_HOME").ok(); + let original_config_home = std::env::var("FRONTAL_CODE_CONFIG_HOME").ok(); let original_codex_home = std::env::var("CODEX_HOME").ok(); let original_dir = std::env::current_dir().expect("cwd"); std::env::set_var("HOME", &home); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); std::env::remove_var("CODEX_HOME"); std::env::set_current_dir(&nested).expect("set cwd"); @@ -7455,8 +7455,8 @@ mod tests { None => std::env::remove_var("HOME"), } match original_config_home { - Some(value) => std::env::set_var("FCODE_CONFIG_HOME", value), - None => std::env::remove_var("FCODE_CONFIG_HOME"), + Some(value) => std::env::set_var("FRONTAL_CODE_CONFIG_HOME", value), + None => std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"), } match original_codex_home { Some(value) => std::env::set_var("CODEX_HOME", value), @@ -7490,11 +7490,11 @@ mod tests { .expect("agents skill file should exist"); let original_home = std::env::var("HOME").ok(); - let original_config_home = std::env::var("FCODE_CONFIG_HOME").ok(); + let original_config_home = std::env::var("FRONTAL_CODE_CONFIG_HOME").ok(); let original_codex_home = std::env::var("CODEX_HOME").ok(); let original_dir = std::env::current_dir().expect("cwd"); std::env::set_var("HOME", &home); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); std::env::remove_var("CODEX_HOME"); std::env::set_current_dir(&nested).expect("set cwd"); @@ -7526,8 +7526,8 @@ mod tests { None => std::env::remove_var("HOME"), } match original_config_home { - Some(value) => std::env::set_var("FCODE_CONFIG_HOME", value), - None => std::env::remove_var("FCODE_CONFIG_HOME"), + Some(value) => std::env::set_var("FRONTAL_CODE_CONFIG_HOME", value), + None => std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"), } match original_codex_home { Some(value) => std::env::set_var("CODEX_HOME", value), @@ -7555,11 +7555,11 @@ mod tests { .expect("learned skill file should exist"); let original_home = std::env::var("HOME").ok(); - let original_config_home = std::env::var("FCODE_CONFIG_HOME").ok(); + let original_config_home = std::env::var("FRONTAL_CODE_CONFIG_HOME").ok(); let original_codex_home = std::env::var("CODEX_HOME").ok(); let original_claude_config_dir = std::env::var("CLAUDE_CONFIG_DIR").ok(); std::env::set_var("HOME", &home); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); std::env::remove_var("CODEX_HOME"); std::env::set_var("CLAUDE_CONFIG_DIR", &claude_config_dir); @@ -7578,8 +7578,8 @@ mod tests { None => std::env::remove_var("HOME"), } match original_config_home { - Some(value) => std::env::set_var("FCODE_CONFIG_HOME", value), - None => std::env::remove_var("FCODE_CONFIG_HOME"), + Some(value) => std::env::set_var("FRONTAL_CODE_CONFIG_HOME", value), + None => std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"), } match original_codex_home { Some(value) => std::env::set_var("CODEX_HOME", value), @@ -7615,11 +7615,11 @@ mod tests { .expect("direct command file should exist"); let original_home = std::env::var("HOME").ok(); - let original_config_home = std::env::var("FCODE_CONFIG_HOME").ok(); + let original_config_home = std::env::var("FRONTAL_CODE_CONFIG_HOME").ok(); let original_codex_home = std::env::var("CODEX_HOME").ok(); let original_claude_config_dir = std::env::var("CLAUDE_CONFIG_DIR").ok(); std::env::set_var("HOME", &home); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); std::env::remove_var("CODEX_HOME"); std::env::set_var("CLAUDE_CONFIG_DIR", &claude_config_dir); @@ -7651,8 +7651,8 @@ mod tests { None => std::env::remove_var("HOME"), } match original_config_home { - Some(value) => std::env::set_var("FCODE_CONFIG_HOME", value), - None => std::env::remove_var("FCODE_CONFIG_HOME"), + Some(value) => std::env::set_var("FRONTAL_CODE_CONFIG_HOME", value), + None => std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"), } match original_codex_home { Some(value) => std::env::set_var("CODEX_HOME", value), @@ -7683,11 +7683,11 @@ mod tests { .expect("legacy command file should exist"); let original_home = std::env::var("HOME").ok(); - let original_config_home = std::env::var("FCODE_CONFIG_HOME").ok(); + let original_config_home = std::env::var("FRONTAL_CODE_CONFIG_HOME").ok(); let original_codex_home = std::env::var("CODEX_HOME").ok(); let original_dir = std::env::current_dir().expect("cwd"); std::env::set_var("HOME", &home); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); std::env::remove_var("CODEX_HOME"); std::env::set_current_dir(&nested).expect("set cwd"); @@ -7707,8 +7707,8 @@ mod tests { None => std::env::remove_var("HOME"), } match original_config_home { - Some(value) => std::env::set_var("FCODE_CONFIG_HOME", value), - None => std::env::remove_var("FCODE_CONFIG_HOME"), + Some(value) => std::env::set_var("FRONTAL_CODE_CONFIG_HOME", value), + None => std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"), } match original_codex_home { Some(value) => std::env::set_var("CODEX_HOME", value), @@ -7756,7 +7756,7 @@ mod tests { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let dir = temp_path("agent-store"); - std::env::set_var("FCODE_AGENT_STORE", &dir); + std::env::set_var("FRONTAL_CODE_AGENT_STORE", &dir); let captured = Arc::new(Mutex::new(None::)); let captured_for_spawn = Arc::clone(&captured); @@ -7777,7 +7777,7 @@ mod tests { }, ) .expect("Agent should succeed"); - std::env::remove_var("FCODE_AGENT_STORE"); + std::env::remove_var("FRONTAL_CODE_AGENT_STORE"); assert_eq!(manifest.name, "ship-audit"); assert_eq!(manifest.subagent_type.as_deref(), Some("Explore")); @@ -7841,7 +7841,7 @@ mod tests { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let dir = temp_path("agent-cancel"); - std::env::set_var("FCODE_AGENT_STORE", &dir); + std::env::set_var("FRONTAL_CODE_AGENT_STORE", &dir); let manifest = execute_agent_with_spawn( AgentInput { @@ -7877,7 +7877,7 @@ mod tests { assert!(output_contents.contains("sub-agent cancelled by control plane")); unregister_hosted_agent_control(&manifest.agent_id); - std::env::remove_var("FCODE_AGENT_STORE"); + std::env::remove_var("FRONTAL_CODE_AGENT_STORE"); let _ = std::fs::remove_dir_all(dir); } @@ -7887,7 +7887,7 @@ mod tests { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let dir = temp_path("agent-status"); - std::env::set_var("FCODE_AGENT_STORE", &dir); + std::env::set_var("FRONTAL_CODE_AGENT_STORE", &dir); let manifest = execute_agent_with_spawn( AgentInput { @@ -7922,7 +7922,7 @@ mod tests { let wrong_store = temp_path("agent-status-wrong-store"); std::fs::create_dir_all(&wrong_store).expect("wrong store dir should exist"); - std::env::set_var("FCODE_AGENT_STORE", &wrong_store); + std::env::set_var("FRONTAL_CODE_AGENT_STORE", &wrong_store); let restored_from_locator = hosted_agent_status_with_locator(&HostedAgentLocator { agent_id: Some(manifest.agent_id.clone()), manifest_file: Some(manifest.manifest_file.clone()), @@ -7946,7 +7946,7 @@ mod tests { Some("hosted agent manifest restored from locator path") ); - std::env::remove_var("FCODE_AGENT_STORE"); + std::env::remove_var("FRONTAL_CODE_AGENT_STORE"); let _ = std::fs::remove_dir_all(dir); let _ = std::fs::remove_dir_all(wrong_store); } @@ -7958,7 +7958,7 @@ mod tests { .unwrap_or_else(std::sync::PoisonError::into_inner); let dir = temp_path("agent-status-terminal"); std::fs::create_dir_all(&dir).expect("agent store dir should exist"); - std::env::set_var("FCODE_AGENT_STORE", &dir); + std::env::set_var("FRONTAL_CODE_AGENT_STORE", &dir); let manifest = AgentOutput { agent_id: "agent-status-terminal".to_string(), @@ -7996,7 +7996,7 @@ mod tests { assert!(!restored.orphaned); assert_eq!(restored.status.as_deref(), Some("completed")); - std::env::remove_var("FCODE_AGENT_STORE"); + std::env::remove_var("FRONTAL_CODE_AGENT_STORE"); let _ = std::fs::remove_dir_all(dir); } @@ -8007,7 +8007,7 @@ mod tests { .unwrap_or_else(std::sync::PoisonError::into_inner); let dir = temp_path("agent-cancel-fallback"); std::fs::create_dir_all(&dir).expect("agent store dir should exist"); - std::env::set_var("FCODE_AGENT_STORE", &dir); + std::env::set_var("FRONTAL_CODE_AGENT_STORE", &dir); let manifest = AgentOutput { agent_id: "agent-cancel-fallback".to_string(), @@ -8036,7 +8036,7 @@ mod tests { let wrong_store = temp_path("agent-cancel-fallback-wrong-store"); std::fs::create_dir_all(&wrong_store).expect("wrong store dir should exist"); - std::env::set_var("FCODE_AGENT_STORE", &wrong_store); + std::env::set_var("FRONTAL_CODE_AGENT_STORE", &wrong_store); let cancellation = cancel_hosted_agent_with_locator(&HostedAgentLocator { agent_id: Some(manifest.agent_id.clone()), @@ -8061,7 +8061,7 @@ mod tests { assert_eq!(manifest_json["derivedState"], "cancelled"); assert!(output_contents.contains("sub-agent cancelled by control plane after restart")); - std::env::remove_var("FCODE_AGENT_STORE"); + std::env::remove_var("FRONTAL_CODE_AGENT_STORE"); let _ = std::fs::remove_dir_all(dir); let _ = std::fs::remove_dir_all(wrong_store); } @@ -8073,7 +8073,10 @@ mod tests { .unwrap_or_else(std::sync::PoisonError::into_inner); let dir = temp_path("agent-cancel-missing-locator"); std::fs::create_dir_all(&dir).expect("missing locator dir should exist"); - std::env::set_var("FCODE_AGENT_STORE", temp_path("agent-cancel-missing-store")); + std::env::set_var( + "FRONTAL_CODE_AGENT_STORE", + temp_path("agent-cancel-missing-store"), + ); let cancellation = cancel_hosted_agent_with_locator(&HostedAgentLocator { agent_id: Some("missing-agent".to_string()), @@ -8088,7 +8091,7 @@ mod tests { .detail .contains("no hosted agent control or manifest found")); - std::env::remove_var("FCODE_AGENT_STORE"); + std::env::remove_var("FRONTAL_CODE_AGENT_STORE"); let _ = std::fs::remove_dir_all(dir); } @@ -8099,7 +8102,7 @@ mod tests { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let dir = temp_path("agent-runner"); - std::env::set_var("FCODE_AGENT_STORE", &dir); + std::env::set_var("FRONTAL_CODE_AGENT_STORE", &dir); let completed = execute_agent_with_spawn( AgentInput { @@ -8234,7 +8237,7 @@ mod tests { ); assert_eq!(spawn_error_manifest_json["derivedState"], "truly_idle"); - std::env::remove_var("FCODE_AGENT_STORE"); + std::env::remove_var("FRONTAL_CODE_AGENT_STORE"); let _ = std::fs::remove_dir_all(dir); } @@ -8243,7 +8246,10 @@ mod tests { let _guard = env_lock() .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - std::env::set_var("FCODE_SERVER_URL", "http://hosted.frontal-code.test/"); + std::env::set_var( + "FRONTAL_CODE_SERVER_URL", + "http://hosted.frontal-code.test/", + ); let manifest = AgentOutput { agent_id: "agent-success".to_string(), name: "hosted-report-success".to_string(), @@ -8277,7 +8283,7 @@ mod tests { assert_eq!(request_json["finish_reason"], "stop"); assert_eq!(request_json["result"], "Hosted completion reported"); assert!(request_json.get("error").is_none()); - std::env::remove_var("FCODE_SERVER_URL"); + std::env::remove_var("FRONTAL_CODE_SERVER_URL"); } #[test] @@ -8285,7 +8291,10 @@ mod tests { let _guard = env_lock() .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - std::env::set_var("FCODE_SERVER_URL", "http://hosted.frontal-code.test/"); + std::env::set_var( + "FRONTAL_CODE_SERVER_URL", + "http://hosted.frontal-code.test/", + ); let manifest = AgentOutput { agent_id: "agent-failure".to_string(), name: "hosted-report-failure".to_string(), @@ -8322,7 +8331,7 @@ mod tests { "tool failed: simulated hosted failure" ); assert!(request_json.get("result").is_none()); - std::env::remove_var("FCODE_SERVER_URL"); + std::env::remove_var("FRONTAL_CODE_SERVER_URL"); } #[test] @@ -8365,7 +8374,10 @@ mod tests { let _guard = env_lock() .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - std::env::set_var("FCODE_SERVER_URL", "http://hosted.frontal-code.test/"); + std::env::set_var( + "FRONTAL_CODE_SERVER_URL", + "http://hosted.frontal-code.test/", + ); let manifest = AgentOutput { agent_id: "agent-cancelled".to_string(), name: "hosted-report-cancelled".to_string(), @@ -8393,7 +8405,7 @@ mod tests { ); assert!(request.is_none()); - std::env::remove_var("FCODE_SERVER_URL"); + std::env::remove_var("FRONTAL_CODE_SERVER_URL"); } #[test] @@ -9145,10 +9157,10 @@ mod tests { .expect("write global settings"); let original_home = std::env::var("HOME").ok(); - let original_config_home = std::env::var("FCODE_CONFIG_HOME").ok(); + let original_config_home = std::env::var("FRONTAL_CODE_CONFIG_HOME").ok(); let original_dir = std::env::current_dir().expect("cwd"); std::env::set_var("HOME", &home); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); std::env::set_current_dir(&cwd).expect("set cwd"); let get = execute_tool("Config", &json!({"setting": "verbose"})).expect("get config"); @@ -9182,8 +9194,8 @@ mod tests { None => std::env::remove_var("HOME"), } match original_config_home { - Some(value) => std::env::set_var("FCODE_CONFIG_HOME", value), - None => std::env::remove_var("FCODE_CONFIG_HOME"), + Some(value) => std::env::set_var("FRONTAL_CODE_CONFIG_HOME", value), + None => std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"), } let _ = std::fs::remove_dir_all(root); } @@ -9211,10 +9223,10 @@ mod tests { .expect("write local settings"); let original_home = std::env::var("HOME").ok(); - let original_config_home = std::env::var("FCODE_CONFIG_HOME").ok(); + let original_config_home = std::env::var("FRONTAL_CODE_CONFIG_HOME").ok(); let original_dir = std::env::current_dir().expect("cwd"); std::env::set_var("HOME", &home); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); std::env::set_current_dir(&cwd).expect("set cwd"); let enter = execute_tool("EnterPlanMode", &json!({})).expect("enter plan mode"); @@ -9260,8 +9272,8 @@ mod tests { None => std::env::remove_var("HOME"), } match original_config_home { - Some(value) => std::env::set_var("FCODE_CONFIG_HOME", value), - None => std::env::remove_var("FCODE_CONFIG_HOME"), + Some(value) => std::env::set_var("FRONTAL_CODE_CONFIG_HOME", value), + None => std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"), } let _ = std::fs::remove_dir_all(root); } @@ -9284,10 +9296,10 @@ mod tests { std::fs::create_dir_all(cwd.join(".frontal-code")).expect("cwd dir"); let original_home = std::env::var("HOME").ok(); - let original_config_home = std::env::var("FCODE_CONFIG_HOME").ok(); + let original_config_home = std::env::var("FRONTAL_CODE_CONFIG_HOME").ok(); let original_dir = std::env::current_dir().expect("cwd"); std::env::set_var("HOME", &home); - std::env::remove_var("FCODE_CONFIG_HOME"); + std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"); std::env::set_current_dir(&cwd).expect("set cwd"); let enter = execute_tool("EnterPlanMode", &json!({})).expect("enter plan mode"); @@ -9322,8 +9334,8 @@ mod tests { None => std::env::remove_var("HOME"), } match original_config_home { - Some(value) => std::env::set_var("FCODE_CONFIG_HOME", value), - None => std::env::remove_var("FCODE_CONFIG_HOME"), + Some(value) => std::env::set_var("FRONTAL_CODE_CONFIG_HOME", value), + None => std::env::remove_var("FRONTAL_CODE_CONFIG_HOME"), } let _ = std::fs::remove_dir_all(root); } @@ -10157,14 +10169,14 @@ printf 'pwsh:%s' "$1" .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let _env = EnvRestoreGuard::capture(&[ - "FCODE_MEMORY_METADATA_PATH", - "FCODE_MEMORY_PINECONE_URL", - "FCODE_MEMORY_PINECONE_NAMESPACE", - "FCODE_MEMORY_PINECONE_API_KEY", - "FCODE_MEMORY_NEO4J_URL", - "FCODE_MEMORY_NEO4J_DATABASE", - "FCODE_MEMORY_NEO4J_USERNAME", - "FCODE_MEMORY_NEO4J_PASSWORD", + "FRONTAL_CODE_MEMORY_METADATA_PATH", + "FRONTAL_CODE_MEMORY_PINECONE_URL", + "FRONTAL_CODE_MEMORY_PINECONE_NAMESPACE", + "FRONTAL_CODE_MEMORY_PINECONE_API_KEY", + "FRONTAL_CODE_MEMORY_NEO4J_URL", + "FRONTAL_CODE_MEMORY_NEO4J_DATABASE", + "FRONTAL_CODE_MEMORY_NEO4J_USERNAME", + "FRONTAL_CODE_MEMORY_NEO4J_PASSWORD", ]); let metadata_path = temp_path("memory-tool-backend.tsv"); @@ -10212,23 +10224,23 @@ printf 'pwsh:%s' "$1" })) }; - std::env::set_var("FCODE_MEMORY_METADATA_PATH", &metadata_path); + std::env::set_var("FRONTAL_CODE_MEMORY_METADATA_PATH", &metadata_path); std::env::set_var( - "FCODE_MEMORY_PINECONE_URL", + "FRONTAL_CODE_MEMORY_PINECONE_URL", format!("http://{}", pinecone_server.addr()), ); - std::env::set_var("FCODE_MEMORY_PINECONE_NAMESPACE", "memories"); - if std::env::var("FCODE_MEMORY_PINECONE_API_KEY").is_err() { - std::env::set_var("FCODE_MEMORY_PINECONE_API_KEY", "pinecone-secret"); + std::env::set_var("FRONTAL_CODE_MEMORY_PINECONE_NAMESPACE", "memories"); + if std::env::var("FRONTAL_CODE_MEMORY_PINECONE_API_KEY").is_err() { + std::env::set_var("FRONTAL_CODE_MEMORY_PINECONE_API_KEY", "pinecone-secret"); } std::env::set_var( - "FCODE_MEMORY_NEO4J_URL", + "FRONTAL_CODE_MEMORY_NEO4J_URL", format!("http://{}", neo4j_server.addr()), ); - std::env::set_var("FCODE_MEMORY_NEO4J_DATABASE", "neo4j"); - std::env::set_var("FCODE_MEMORY_NEO4J_USERNAME", "neo4j"); - if std::env::var("FCODE_MEMORY_NEO4J_PASSWORD").is_err() { - std::env::set_var("FCODE_MEMORY_NEO4J_PASSWORD", "neo4j-secret"); + std::env::set_var("FRONTAL_CODE_MEMORY_NEO4J_DATABASE", "neo4j"); + std::env::set_var("FRONTAL_CODE_MEMORY_NEO4J_USERNAME", "neo4j"); + if std::env::var("FRONTAL_CODE_MEMORY_NEO4J_PASSWORD").is_err() { + std::env::set_var("FRONTAL_CODE_MEMORY_NEO4J_PASSWORD", "neo4j-secret"); } let registry = GlobalToolRegistry::builtin(); diff --git a/crates/training/BUILD.bazel b/crates/training/BUILD.bazel index f8fe1e9..cc9b17f 100644 --- a/crates/training/BUILD.bazel +++ b/crates/training/BUILD.bazel @@ -11,6 +11,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-training", "test", @@ -23,7 +24,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-training", "fmt-check", @@ -37,6 +40,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-training", "clippy", diff --git a/crates/webhooks/BUILD.bazel b/crates/webhooks/BUILD.bazel index 316a54c..3cf4ab3 100644 --- a/crates/webhooks/BUILD.bazel +++ b/crates/webhooks/BUILD.bazel @@ -11,6 +11,7 @@ filegroup( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-webhooks", "test", @@ -23,7 +24,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-webhooks", "fmt-check", @@ -37,6 +40,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-webhooks", "clippy", diff --git a/deploy.yaml b/deploy.yaml index 762ba10..f7d0e72 100644 --- a/deploy.yaml +++ b/deploy.yaml @@ -3,13 +3,13 @@ primary_region: iad build: dockerfile: infrastructure/docker/frontal-code-fly.Dockerfile env: - FCODE_SERVER_HOST: "0.0.0.0" - FCODE_SERVER_PORT: "8788" - FCODE_SERVER_LANE_TRANSPORT: "tools-agent" - FCODE_SERVER_STATE_FILE: "/var/lib/frontal-code/server/state.json" - FCODE_SERVER_WORKSPACE_ROOT: "/workspace/workspaces" - FCODE_SERVER_RECONCILE_INTERVAL_SECS: "15" - FCODE_AGENT_STORE: "/var/lib/frontal-code/agents" + FRONTAL_CODE_SERVER_HOST: "0.0.0.0" + FRONTAL_CODE_SERVER_PORT: "8788" + FRONTAL_CODE_SERVER_LANE_TRANSPORT: "tools-agent" + FRONTAL_CODE_SERVER_STATE_FILE: "/var/lib/frontal-code/server/state.json" + FRONTAL_CODE_SERVER_WORKSPACE_ROOT: "/workspace/workspaces" + FRONTAL_CODE_SERVER_RECONCILE_INTERVAL_SECS: "15" + FRONTAL_CODE_AGENT_STORE: "/var/lib/frontal-code/agents" RUST_LOG: "info" http_service: internal_port: 8788 @@ -33,13 +33,13 @@ machines: size: shared-cpu-1x region: iad env: - FCODE_SERVER_HOST: "0.0.0.0" - FCODE_SERVER_PORT: "8788" - FCODE_SERVER_LANE_TRANSPORT: "tools-agent" - FCODE_SERVER_STATE_FILE: "/var/lib/frontal-code/server/state.json" - FCODE_SERVER_WORKSPACE_ROOT: "/workspace/workspaces" - FCODE_SERVER_RECONCILE_INTERVAL_SECS: "15" - FCODE_AGENT_STORE: "/var/lib/frontal-code/agents" + FRONTAL_CODE_SERVER_HOST: "0.0.0.0" + FRONTAL_CODE_SERVER_PORT: "8788" + FRONTAL_CODE_SERVER_LANE_TRANSPORT: "tools-agent" + FRONTAL_CODE_SERVER_STATE_FILE: "/var/lib/frontal-code/server/state.json" + FRONTAL_CODE_SERVER_WORKSPACE_ROOT: "/workspace/workspaces" + FRONTAL_CODE_SERVER_RECONCILE_INTERVAL_SECS: "15" + FRONTAL_CODE_AGENT_STORE: "/var/lib/frontal-code/agents" RUST_LOG: "info" NODE_ENV: "production" mounts: @@ -56,13 +56,13 @@ machines: size: shared-cpu-1x region: iad env: - FCODE_SERVER_HOST: "0.0.0.0" - FCODE_SERVER_PORT: "8788" - FCODE_SERVER_LANE_TRANSPORT: "tools-agent" - FCODE_SERVER_STATE_FILE: "/var/lib/frontal-code/server/state.json" - FCODE_SERVER_WORKSPACE_ROOT: "/workspace/workspaces" - FCODE_SERVER_RECONCILE_INTERVAL_SECS: "15" - FCODE_AGENT_STORE: "/var/lib/frontal-code/agents" + FRONTAL_CODE_SERVER_HOST: "0.0.0.0" + FRONTAL_CODE_SERVER_PORT: "8788" + FRONTAL_CODE_SERVER_LANE_TRANSPORT: "tools-agent" + FRONTAL_CODE_SERVER_STATE_FILE: "/var/lib/frontal-code/server/state.json" + FRONTAL_CODE_SERVER_WORKSPACE_ROOT: "/workspace/workspaces" + FRONTAL_CODE_SERVER_RECONCILE_INTERVAL_SECS: "15" + FRONTAL_CODE_AGENT_STORE: "/var/lib/frontal-code/agents" RUST_LOG: "debug" NODE_ENV: "development" mounts: diff --git a/docs/CLI_REFERENCES.md b/docs/CLI_REFERENCES.md index 9735bfd..7009f9b 100644 --- a/docs/CLI_REFERENCES.md +++ b/docs/CLI_REFERENCES.md @@ -672,10 +672,10 @@ frontal-code prompt --stream "Generate a long story" | Variable | Description | Default | |----------|-------------|---------| -| `FCODE_CONFIG_DIR` | Configuration directory | `~/.frontal-code` | -| `FCODE_DATA_DIR` | Data directory | `~/.frontal-code/data` | -| `FCODE_LOG_LEVEL` | Log level | `info` | -| `FCODE_SESSION_DIR` | Session directory | `~/.frontal-code/sessions` | +| `FRONTAL_CODE_CONFIG_DIR` | Configuration directory | `~/.frontal-code` | +| `FRONTAL_CODE_DATA_DIR` | Data directory | `~/.frontal-code/data` | +| `FRONTAL_CODE_LOG_LEVEL` | Log level | `info` | +| `FRONTAL_CODE_SESSION_DIR` | Session directory | `~/.frontal-code/sessions` | | `RUST_LOG` | Rust log level | `info` | ## Configuration Files diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 3d1d7c4..59c329c 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -38,26 +38,26 @@ export FRONTAL_BASE_URL="https://ai.frontal.dev/v1" ```bash # General settings -export FCODE_LOG_LEVEL="info" # debug, info, warn, error -export FCODE_CONFIG_DIR="$HOME/.frontal-code" -export FCODE_DATA_DIR="$HOME/.frontal-code/data" +export FRONTAL_CODE_LOG_LEVEL="info" # debug, info, warn, error +export FRONTAL_CODE_CONFIG_DIR="$HOME/.frontal-code" +export FRONTAL_CODE_DATA_DIR="$HOME/.frontal-code/data" # Provider selection -export FCODE_DEFAULT_PROVIDER="anthropic" # anthropic, openai, xai -export FCODE_DEFAULT_MODEL="claude-opus-5" +export FRONTAL_CODE_DEFAULT_PROVIDER="anthropic" # anthropic, openai, xai +export FRONTAL_CODE_DEFAULT_MODEL="claude-opus-5" # Permission settings -export FCODE_PERMISSION_MODE="danger-full-access" # danger-full-access, safe-mode, ask-permissions -export FCODE_ALLOWED_TOOLS="bash,read,write,edit,grep" +export FRONTAL_CODE_PERMISSION_MODE="danger-full-access" # danger-full-access, safe-mode, ask-permissions +export FRONTAL_CODE_ALLOWED_TOOLS="bash,read,write,edit,grep" # Session settings -export FCODE_SESSION_DIR="$HOME/.frontal-code/sessions" -export FCODE_AUTO_SAVE_SESSIONS="true" -export FCODE_MAX_SESSIONS="100" +export FRONTAL_CODE_SESSION_DIR="$HOME/.frontal-code/sessions" +export FRONTAL_CODE_AUTO_SAVE_SESSIONS="true" +export FRONTAL_CODE_MAX_SESSIONS="100" # MCP settings -export FCODE_MCP_SERVERS_DIR="$HOME/.frontal-code/mcp-servers" -export FCODE_MCP_TIMEOUT="30" +export FRONTAL_CODE_MCP_SERVERS_DIR="$HOME/.frontal-code/mcp-servers" +export FRONTAL_CODE_MCP_TIMEOUT="30" ``` ## Config File Format @@ -96,17 +96,17 @@ The `.frontal-code/settings.json` config file uses JSON format with the followin "session": { "auto_save": true, "max_sessions": 100, - "session_dir": "${FCODE_SESSION_DIR}", + "session_dir": "${FRONTAL_CODE_SESSION_DIR}", "resume_last_session": false }, "mcp": { - "servers_dir": "${FCODE_MCP_SERVERS_DIR}", + "servers_dir": "${FRONTAL_CODE_MCP_SERVERS_DIR}", "timeout": 30, "auto_start": [], "enabled": true }, "plugins": { - "plugins_dir": "${FCODE_CONFIG_DIR}/plugins", + "plugins_dir": "${FRONTAL_CODE_CONFIG_DIR}/plugins", "auto_load": [], "enabled": true }, @@ -395,13 +395,13 @@ frontal-code --show-tool-calls prompt "list files" ```bash # Disable telemetry -export FCODE_TELEMETRY_ENABLED=false +export FRONTAL_CODE_TELEMETRY_ENABLED=false # Set sample rate -export FCODE_TELEMETRY_SAMPLE_RATE=0.1 +export FRONTAL_CODE_TELEMETRY_SAMPLE_RATE=0.1 # Custom endpoint -export FCODE_TELEMETRY_ENDPOINT="https://my-telemetry.example.com" +export FRONTAL_CODE_TELEMETRY_ENDPOINT="https://my-telemetry.example.com" ``` ## Advanced Configuration diff --git a/docs/CONTAINERS.md b/docs/CONTAINERS.md index cb05f48..78022c2 100644 --- a/docs/CONTAINERS.md +++ b/docs/CONTAINERS.md @@ -135,7 +135,7 @@ RUN mkdir -p /home/frontal-code/.frontal-code && \ USER frontal-code # Set environment -ENV FCODE_DATA_DIR=/home/frontal-code/.frontal-code +ENV FRONTAL_CODE_DATA_DIR=/home/frontal-code/.frontal-code ENV PATH=/usr/local/bin:$PATH # Health check @@ -200,9 +200,9 @@ services: - ./:/app - frontal-code-data:/home/frontal-code/.frontal-code environment: - - FCODE_LOG_LEVEL=debug + - FRONTAL_CODE_LOG_LEVEL=debug - FRONTAL_API_KEY=${FRONTAL_API_KEY} - - FCODE_DEFAULT_MODEL=claude-sonnet-4-6 + - FRONTAL_CODE_DEFAULT_MODEL=claude-sonnet-4-6 working_dir: /app command: repl restart: unless-stopped @@ -238,9 +238,9 @@ services: delay: 5s max_attempts: 3 environment: - - FCODE_LOG_LEVEL=info + - FRONTAL_CODE_LOG_LEVEL=info - FRONTAL_API_KEY=${FRONTAL_API_KEY} - - FCODE_PERMISSION_MODE=safe-mode + - FRONTAL_CODE_PERMISSION_MODE=safe-mode volumes: - frontal-code-config:/home/frontal-code/.frontal-code - frontal-code-sessions:/home/frontal-code/.frontal-code/sessions @@ -286,8 +286,8 @@ services: frontal-code: image: frontal-code/cli:v0.1.0 environment: - - FCODE_TELEMETRY_ENABLED=true - - FCODE_TELEMETRY_ENDPOINT=http://prometheus:9090/metrics + - FRONTAL_CODE_TELEMETRY_ENABLED=true + - FRONTAL_CODE_TELEMETRY_ENDPOINT=http://prometheus:9090/metrics depends_on: - prometheus - grafana @@ -414,16 +414,16 @@ spec: containerPort: 8080 protocol: TCP env: - - name: FCODE_LOG_LEVEL + - name: FRONTAL_CODE_LOG_LEVEL value: "info" - name: FRONTAL_API_KEY valueFrom: secretKeyRef: name: frontal-code-secrets key: anthropic-api-key - - name: FCODE_DEFAULT_MODEL + - name: FRONTAL_CODE_DEFAULT_MODEL value: "claude-sonnet-4-6" - - name: FCODE_PERMISSION_MODE + - name: FRONTAL_CODE_PERMISSION_MODE value: "safe-mode" resources: requests: @@ -741,11 +741,11 @@ resources: ```yaml env: - - name: FCODE_CONNECTION_POOL_SIZE + - name: FRONTAL_CODE_CONNECTION_POOL_SIZE value: "10" - - name: FCODE_CONNECTION_TIMEOUT + - name: FRONTAL_CODE_CONNECTION_TIMEOUT value: "30" - - name: FCODE_KEEP_ALIVE + - name: FRONTAL_CODE_KEEP_ALIVE value: "true" ``` diff --git a/docs/MCP.md b/docs/MCP.md index 20b5de0..5864079 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -668,8 +668,8 @@ logical name to an MCP `server` plus the tool names it exposes and optional OAut "auth_url": "https://github.com/login/oauth/authorize", "token_url": "https://github.com/login/oauth/access_token", "scopes": ["repo", "read:org", "workflow"], - "client_id_env": "FCODE_GITHUB_CLIENT_ID", - "client_secret_env": "FCODE_GITHUB_CLIENT_SECRET" + "client_id_env": "FRONTAL_CODE_GITHUB_CLIENT_ID", + "client_secret_env": "FRONTAL_CODE_GITHUB_CLIENT_SECRET" } } } diff --git a/docs/SECURITY.md b/docs/SECURITY.md index afec057..456a9c0 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -19,7 +19,7 @@ tasks. Both of its authentication boundaries are closed by default. ### Control plane -Set `FCODE_SERVER_API_KEY` before starting the server. Clients present it as +Set `FRONTAL_CODE_SERVER_API_KEY` before starting the server. Clients present it as either `x-api-key: ` or `Authorization: Bearer `; the comparison is constant-time. @@ -29,7 +29,7 @@ development, or a host reachable only from inside your own perimeter — opt in explicitly: ```bash -FCODE_SERVER_ALLOW_ANONYMOUS=1 frontal-code-server +FRONTAL_CODE_SERVER_ALLOW_ANONYMOUS=1 frontal-code-server ``` That path logs a warning naming the bind address on every start. @@ -38,10 +38,10 @@ That path logs a warning naming the bind address on every start. `POST /v1/webhooks/` sits outside the control-plane auth layer, so its HMAC signature is the only thing protecting it. Each source needs its own -secret, named `FCODE__WEBHOOK_SECRET`: +secret, named `FRONTAL_CODE__WEBHOOK_SECRET`: ```bash -export FCODE_GITHUB_WEBHOOK_SECRET="..." +export FRONTAL_CODE_GITHUB_WEBHOOK_SECRET="..." ``` A delivery is rejected with `401` when the secret is missing or blank, when the diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel index 29de00e..49f8800 100644 --- a/examples/BUILD.bazel +++ b/examples/BUILD.bazel @@ -14,6 +14,7 @@ filegroup( sh_test( name = "core_config_example", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-core", "run", diff --git a/extensions/frontal-code-ide/BUILD.bazel b/extensions/frontal-code-ide/BUILD.bazel index 3067291..c2cbe9f 100644 --- a/extensions/frontal-code-ide/BUILD.bazel +++ b/extensions/frontal-code-ide/BUILD.bazel @@ -14,7 +14,9 @@ filegroup( sh_test( name = "lint", + size = "small", srcs = ["//tools:extension.sh"], + data = [":srcs", "//:biome.json", "//:ultracite.json", "//:.gitignore", "package.json"], args = [ "extensions/frontal-code-ide", "lint", @@ -28,6 +30,7 @@ sh_test( sh_test( name = "build", srcs = ["//tools:extension.sh"], + data = [":srcs", "//:biome.json", "//:ultracite.json", "//:.gitignore", "package.json"], args = [ "extensions/frontal-code-ide", "build", diff --git a/extensions/frontal-code-ide/extension.js b/extensions/frontal-code-ide/extension.js index 724a9e1..6207521 100644 --- a/extensions/frontal-code-ide/extension.js +++ b/extensions/frontal-code-ide/extension.js @@ -1,26 +1,28 @@ const vscode = require("vscode"); -const { execFile } = require("child_process"); +const { execFile } = require("node:child_process"); function activate(context) { context.subscriptions.push( vscode.commands.registerCommand("frontal-code.startRepl", () => startRepl()), vscode.commands.registerCommand("frontal-code.askSelection", () => askSelection()), - vscode.commands.registerCommand("frontal-code.askInput", () => askInput()) + vscode.commands.registerCommand("frontal-code.askInput", () => askInput()), ); } -function deactivate() {} +function deactivate() { + // Intentionally empty: VS Code owns extension teardown. +} function config() { return vscode.workspace.getConfiguration("frontal-code"); } -function frontal-codeCliPath() { +function frontalCodeCliPath() { const value = config().get("cliPath", "frontal-code"); return typeof value === "string" && value.trim().length > 0 ? value.trim() : "frontal-code"; } -function frontal-codeModelArgs() { +function frontalCodeModelArgs() { const model = config().get("defaultModel", ""); if (typeof model === "string" && model.trim().length > 0) { return ["--model", model.trim()]; @@ -31,15 +33,15 @@ function frontal-codeModelArgs() { function startRepl() { const terminal = vscode.window.createTerminal("Frontal Code"); terminal.show(true); - terminal.sendText(frontal-codeCliPath(), true); + terminal.sendText(frontalCodeCliPath(), true); } async function askInput() { const question = await vscode.window.showInputBox({ prompt: "Ask Frontal Code", - placeHolder: "Explain the active file" + placeHolder: "Explain the active file", }); - if (!question || !question.trim()) { + if (!question?.trim()) { return; } await runFrontalCodePrompt(question.trim()); @@ -58,9 +60,9 @@ async function askSelection() { } const question = await vscode.window.showInputBox({ prompt: "What should Frontal Code do with this selection?", - placeHolder: "Explain this code" + placeHolder: "Explain this code", }); - if (!question || !question.trim()) { + if (!question?.trim()) { return; } const prompt = `${question.trim()}\n\nSelected code:\n${selected}`; @@ -72,19 +74,19 @@ async function runFrontalCodePrompt(prompt) { output.show(true); output.appendLine("Running Frontal Code..."); - const args = [...frontal-codeModelArgs(), "--output-format", "text", "prompt", prompt]; + const args = [...frontalCodeModelArgs(), "--output-format", "text", "prompt", prompt]; const cwd = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath; const execOptions = cwd ? { cwd, maxBuffer: 16 * 1024 * 1024 } : { maxBuffer: 16 * 1024 * 1024 }; const startedAt = Date.now(); - execFile(frontal-codeCliPath(), args, execOptions, (error, stdout, stderr) => { + execFile(frontalCodeCliPath(), args, execOptions, (error, stdout, stderr) => { const elapsedMs = Date.now() - startedAt; output.appendLine(`Frontal Code finished in ${elapsedMs} ms.`); - if (stdout && stdout.trim()) { + if (stdout?.trim()) { output.appendLine(""); output.appendLine(stdout.trimEnd()); } - if (stderr && stderr.trim()) { + if (stderr?.trim()) { output.appendLine(""); output.appendLine("stderr:"); output.appendLine(stderr.trimEnd()); @@ -103,5 +105,5 @@ async function runFrontalCodePrompt(prompt) { module.exports = { activate, - deactivate + deactivate, }; diff --git a/extensions/frontal-code-ide/package.json b/extensions/frontal-code-ide/package.json index dfe7550..670834f 100644 --- a/extensions/frontal-code-ide/package.json +++ b/extensions/frontal-code-ide/package.json @@ -11,6 +11,9 @@ "Other" ], "main": "./extension.js", + "scripts": { + "build": "node --check extension.js" + }, "activationEvents": [ "onCommand:frontal-code.startRepl", "onCommand:frontal-code.askSelection", diff --git a/extensions/frontal-code-slack/.env.example b/extensions/frontal-code-slack/.env.example index 6e9149d..605a44e 100644 --- a/extensions/frontal-code-slack/.env.example +++ b/extensions/frontal-code-slack/.env.example @@ -5,12 +5,12 @@ SLACK_SIGNING_SECRET=your-signing-secret # Frontal Code API Configuration # For development with tools.frontal.dev infrastructure: -FCODE_API_URL=http://frontal-code-server:8788 +FRONTAL_CODE_API_URL=http://frontal-code-server:8788 # For local development: -# FCODE_API_URL=http://localhost:8788 +# FRONTAL_CODE_API_URL=http://localhost:8788 # For production: -# FCODE_API_URL=https://tools.frontal.dev/frontal-code -FCODE_API_TIMEOUT=30000 +# FRONTAL_CODE_API_URL=https://tools.frontal.dev/frontal-code +FRONTAL_CODE_API_TIMEOUT=30000 FRONTAL_SERVER_API_KEY=replace-with-your-frontal-code-api-key # Application Configuration @@ -40,6 +40,6 @@ HEALTH_CHECK_INTERVAL=30000 # - Webhooks should be configured to https://tools.frontal.dev/frontal-code/webhooks/slack # Production Environment Configuration -# - Use https://tools.frontal.dev/frontal-code for FCODE_API_URL +# - Use https://tools.frontal.dev/frontal-code for FRONTAL_CODE_API_URL # - Ensure SLACK_BOT_TOKEN has necessary permissions # - Configure SENTRY_DSN for error tracking diff --git a/extensions/frontal-code-slack/BUILD.bazel b/extensions/frontal-code-slack/BUILD.bazel index 9e2d424..999dfd5 100644 --- a/extensions/frontal-code-slack/BUILD.bazel +++ b/extensions/frontal-code-slack/BUILD.bazel @@ -5,7 +5,9 @@ filegroup( srcs = glob( [ "src/**/*", + "scripts/**/*", "tests/**/*", + "package.json", ], exclude = [ "node_modules/**", @@ -19,7 +21,6 @@ filegroup( filegroup( name = "config_srcs", srcs = [ - "biome.json", "bun.lock", "bunfig.toml", "package.json", @@ -30,7 +31,9 @@ filegroup( sh_test( name = "lint", + size = "small", srcs = ["//tools:extension.sh"], + data = [":srcs", ":config_srcs", "//:biome.json", "//:ultracite.json", "//:.gitignore"], args = [ "extensions/frontal-code-slack", "lint", @@ -44,6 +47,7 @@ sh_test( sh_test( name = "build", srcs = ["//tools:extension.sh"], + data = [":srcs", ":config_srcs", "//:biome.json", "//:ultracite.json", "//:.gitignore"], args = [ "extensions/frontal-code-slack", "build", @@ -57,6 +61,7 @@ sh_test( sh_test( name = "test", srcs = ["//tools:extension.sh"], + data = [":srcs", ":config_srcs", "//:biome.json", "//:ultracite.json", "//:.gitignore"], args = [ "extensions/frontal-code-slack", "test", diff --git a/extensions/frontal-code-slack/README.md b/extensions/frontal-code-slack/README.md index c1b9371..f479cf5 100644 --- a/extensions/frontal-code-slack/README.md +++ b/extensions/frontal-code-slack/README.md @@ -55,8 +55,8 @@ SLACK_APP_TOKEN=xapp-your-app-token SLACK_SIGNING_SECRET=your-signing-secret # Frontal Code server -FCODE_API_URL=http://127.0.0.1:8788 -FCODE_API_TIMEOUT=30000 +FRONTAL_CODE_API_URL=http://127.0.0.1:8788 +FRONTAL_CODE_API_TIMEOUT=30000 # App NODE_ENV=development diff --git a/extensions/frontal-code-slack/biome.json b/extensions/frontal-code-slack/biome.json deleted file mode 100644 index 2e735f8..0000000 --- a/extensions/frontal-code-slack/biome.json +++ /dev/null @@ -1,181 +0,0 @@ -{ - "root": false, - "$schema": "https://biomejs.dev/schemas/2.5.14/schema.json", - "assist": { "actions": { "source": { "organizeImports": "on" } } }, - "linter": { - "enabled": true, - "rules": { - "preset": "recommended", - "complexity": { - "noExcessiveCognitiveComplexity": { - "level": "error", - "options": { - "maxAllowedComplexity": 20 - } - }, - "noUselessConstructor": "error", - "useArrowFunction": "error", - "useFlatMap": "error", - "useLiteralKeys": "error", - "useOptionalChain": "error", - "useRegexLiterals": "error", - "useSimpleNumberKeys": "error" - }, - "correctness": { - "noUnusedImports": { - "fix": "safe", - "level": "error" - }, - "noUnusedVariables": "error", - "useIsNan": "error", - "useValidForDirection": "error" - }, - "nursery": { - "useSortedClasses": { - "fix": "safe", - "level": "error", - "options": { - "attributes": ["className"], - "functions": ["clsx", "cva", "tw", "twMerge", "cn", "twJoin", "tv"] - } - } - }, - "performance": { - "noDelete": "error" - }, - "style": { - "noInferrableTypes": "error", - "noNamespace": "error", - "noNonNullAssertion": "error", - "noParameterAssign": "error", - "noParameterProperties": "error", - "noUselessElse": "error", - "noYodaExpression": "error", - "useAsConstAssertion": "error", - "useBlockStatements": { - "fix": "safe", - "level": "error" - }, - "useConsistentArrayType": "error", - "useConsistentBuiltinInstantiation": "error", - "useConst": "error", - "useDefaultParameterLast": "error", - "useDefaultSwitchClause": "error", - "useExportType": "error", - "useForOf": "error", - "useFragmentSyntax": "error", - "useImportType": "error", - "useNumberNamespace": "error", - "useSelfClosingElements": "error", - "useShorthandAssign": "error", - "useShorthandFunctionType": "error", - "useTemplate": "error", - "useThrowNewError": "error", - "useThrowOnlyError": "error" - }, - "suspicious": { - "noApproximativeNumericConstant": "error", - "noAssignInExpressions": "error", - "noAsyncPromiseExecutor": "error", - "noCatchAssign": "error", - "noClassAssign": "error", - "noCommentText": "error", - "noCompareNegZero": "error", - "noConfusingLabels": "error", - "noConfusingVoidType": "error", - "noConstEnum": "error", - "noControlCharactersInRegex": "error", - "noDebugger": "error", - "noDoubleEquals": "error", - "noDuplicateCase": "error", - "noDuplicateClassMembers": "error", - "noDuplicateObjectKeys": "error", - "noDuplicateParameters": "error", - "noEmptyBlockStatements": "error", - "noEmptyInterface": "error", - "noEvolvingTypes": "error", - "noExplicitAny": "error", - "noExtraNonNullAssertion": "error", - "noFallthroughSwitchClause": "error", - "noFunctionAssign": "error", - "noGlobalAssign": "error", - "noGlobalIsFinite": "error", - "noGlobalIsNan": "error", - "noImplicitAnyLet": "error", - "noImportAssign": "error", - "noLabelVar": "error", - "noMisleadingCharacterClass": "error", - "noMisleadingInstantiator": "error", - "noMisrefactoredShorthandAssign": "error", - "noPrototypeBuiltins": "error", - "noRedeclare": "error", - "noRedundantUseStrict": "error", - "noSelfCompare": "error", - "noShadowRestrictedNames": "error", - "noSparseArray": "error", - "noThenProperty": "error", - "noUnsafeDeclarationMerging": "error", - "noUnsafeNegation": "error", - "useAwait": "error", - "useDefaultSwitchClauseLast": "error", - "useErrorMessage": "error", - "useGetterReturn": "error", - "useIsArray": "error", - "useNamespaceKeyword": "error", - "useNumberToFixedDigitsArgument": "error" - } - } - }, - "formatter": { - "enabled": true, - "formatWithErrors": true, - "indentStyle": "space", - "indentWidth": 2, - "lineEnding": "lf", - "lineWidth": 80, - "attributePosition": "auto", - "bracketSpacing": true - }, - "javascript": { - "formatter": { - "arrowParentheses": "always", - "bracketSameLine": false, - "jsxQuoteStyle": "double", - "quoteProperties": "asNeeded", - "semicolons": "always", - "trailingCommas": "es5" - } - }, - "json": { - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2, - "lineEnding": "lf", - "lineWidth": 80, - "trailingCommas": "none" - } - }, - "css": { - "formatter": { - "enabled": true, - "lineEnding": "lf", - "lineWidth": 80, - "quoteStyle": "double" - }, - "parser": { - "allowWrongLineComments": false, - "cssModules": false - } - }, - "files": { - "includes": [ - "**", - "!**/node_modules", - "!**/dist", - "!**/logs", - "!**/*.d.ts", - "!**/src/generated" - ] - } -} diff --git a/extensions/frontal-code-slack/bun.lock b/extensions/frontal-code-slack/bun.lock index 249e996..fef9f36 100644 --- a/extensions/frontal-code-slack/bun.lock +++ b/extensions/frontal-code-slack/bun.lock @@ -15,7 +15,7 @@ "zod": "^3.22.4", }, "devDependencies": { - "@biomejs/biome": "^1.7.3", + "@biomejs/biome": "^2.5.14", "@types/node": "^20.11.30", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", @@ -43,23 +43,23 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, ""], + "@biomejs/biome": ["@biomejs/biome@2.5.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.14", "@biomejs/cli-darwin-x64": "2.5.14", "@biomejs/cli-linux-arm64": "2.5.14", "@biomejs/cli-linux-arm64-musl": "2.5.14", "@biomejs/cli-linux-x64": "2.5.14", "@biomejs/cli-linux-x64-musl": "2.5.14", "@biomejs/cli-win32-arm64": "2.5.14", "@biomejs/cli-win32-x64": "2.5.14" }, "bin": { "biome": "bin/biome" } }, "sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, ""], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.14", "", { "os": "win32", "cpu": "x64" }, "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ=="], "@colors/colors": ["@colors/colors@1.6.0", "", {}, ""], @@ -941,7 +941,7 @@ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, ""], - "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, ""], "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, ""], @@ -953,7 +953,7 @@ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, ""], - "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, ""], "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], @@ -1119,13 +1119,11 @@ "ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "axios/form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, ""], - "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, ""], "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, ""], - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, ""], + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "cssstyle/rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, ""], @@ -1149,12 +1147,6 @@ "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "tinyglobby/fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - - "vitest/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - "winston/is-stream": ["is-stream@2.0.1", "", {}, ""], "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], diff --git a/extensions/frontal-code-slack/docs/DEPLOYMENT.md b/extensions/frontal-code-slack/docs/DEPLOYMENT.md index 42ef627..b82240f 100644 --- a/extensions/frontal-code-slack/docs/DEPLOYMENT.md +++ b/extensions/frontal-code-slack/docs/DEPLOYMENT.md @@ -108,7 +108,7 @@ spec: secretKeyRef: name: slack-secrets key: signing-secret - - name: FCODE_API_URL + - name: FRONTAL_CODE_API_URL value: "http://frontal-code-api:8788" resources: requests: @@ -217,8 +217,8 @@ SLACK_APP_TOKEN=xapp-production-app-token SLACK_SIGNING_SECRET=production-signing-secret # Frontal Code Server -FCODE_API_URL=https://frontal-code-api.your-domain.com -FCODE_API_TIMEOUT=30000 +FRONTAL_CODE_API_URL=https://frontal-code-api.your-domain.com +FRONTAL_CODE_API_TIMEOUT=30000 # Application NODE_ENV=production diff --git a/extensions/frontal-code-slack/docs/README.md b/extensions/frontal-code-slack/docs/README.md index 33a325d..ffc17a1 100644 --- a/extensions/frontal-code-slack/docs/README.md +++ b/extensions/frontal-code-slack/docs/README.md @@ -43,8 +43,8 @@ SLACK_APP_TOKEN=xapp-your-app-token SLACK_SIGNING_SECRET=your-signing-secret # Frontal Code Server -FCODE_API_URL=http://127.0.0.1:8788 -FCODE_API_TIMEOUT=30000 +FRONTAL_CODE_API_URL=http://127.0.0.1:8788 +FRONTAL_CODE_API_TIMEOUT=30000 # Application NODE_ENV=development @@ -198,7 +198,7 @@ Enable: **Tasks not creating:** - Confirm Frontal Code server is accessible -- Check `FCODE_API_URL` configuration +- Check `FRONTAL_CODE_API_URL` configuration - Verify API authentication **WebSocket connection issues:** diff --git a/extensions/frontal-code-slack/docs/TROUBLESHOOTING.md b/extensions/frontal-code-slack/docs/TROUBLESHOOTING.md index bd4d36e..05b4103 100644 --- a/extensions/frontal-code-slack/docs/TROUBLESHOOTING.md +++ b/extensions/frontal-code-slack/docs/TROUBLESHOOTING.md @@ -111,21 +111,21 @@ telnet api.slack.com 443 1. **Check Frontal Code API Connection** ```bash # Test API connectivity - curl -f $FCODE_API_URL/health + curl -f $FRONTAL_CODE_API_URL/health # Check API timeout - curl -w "@curl-format.txt" -o /dev/null -s "$FCODE_API_URL/health" + curl -w "@curl-format.txt" -o /dev/null -s "$FRONTAL_CODE_API_URL/health" ``` 2. **Verify API Configuration** ```bash # Check API URL format - echo $FCODE_API_URL | grep -E "^https?://" + echo $FRONTAL_CODE_API_URL | grep -E "^https?://" # Test with curl curl -H "Content-Type: application/json" \ -d '{"prompt":"test","source":"troubleshoot"}' \ - "$FCODE_API_URL/tasks" + "$FRONTAL_CODE_API_URL/tasks" ``` 3. **Check Request Logs** @@ -148,10 +148,10 @@ telnet api.slack.com 443 **API Connection Issues:** ```bash # Update API URL in .env -FCODE_API_URL=https://frontal-code-api.your-domain.com +FRONTAL_CODE_API_URL=https://frontal-code-api.your-domain.com # Increase timeout -FCODE_API_TIMEOUT=60000 +FRONTAL_CODE_API_TIMEOUT=60000 # Restart service systemctl restart frontal-code-slack @@ -170,7 +170,7 @@ const request = { **Authentication Issues:** ```bash # Add API token if required -FCODE_API_TOKEN=your-api-token-here +FRONTAL_CODE_API_TOKEN=your-api-token-here ``` ### 3. WebSocket Connection Issues @@ -502,7 +502,7 @@ class MessageQueue { 1. **Check Environment Variables** ```bash # Verify all required variables - env | grep -E "^(SLACK|FCODE|NODE_ENV)" + env | grep -E "^(SLACK|FRONTAL_CODE|NODE_ENV)" # Check for missing variables comm -23 <(sort .env.example) <(sort .env) @@ -548,7 +548,7 @@ import Joi from 'joi'; const configSchema = Joi.object({ SLACK_BOT_TOKEN: Joi.string().pattern(/^xoxb-/).required(), SLACK_APP_TOKEN: Joi.string().pattern(/^xapp-/).required(), - FCODE_API_URL: Joi.string().uri().required(), + FRONTAL_CODE_API_URL: Joi.string().uri().required(), PORT: Joi.number().port().default(3000) }); @@ -587,7 +587,7 @@ telnet frontal-code-api.your-domain.com 8788 # Test HTTP connectivity curl -v https://api.slack.com/api/auth.test -curl -v $FCODE_API_URL/health +curl -v $FRONTAL_CODE_API_URL/health ``` ### Performance Profiling diff --git a/extensions/frontal-code-slack/package-lock.json b/extensions/frontal-code-slack/package-lock.json index 67af657..67c3b6a 100644 --- a/extensions/frontal-code-slack/package-lock.json +++ b/extensions/frontal-code-slack/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "frontal-code-slack", "version": "0.1.0", - "license": "MIT", + "license": "Apache-2.0", "dependencies": { "@sentry/node": "^8.22.0", "@slack/bolt": "^3.21.0", @@ -19,7 +19,7 @@ "zod": "^3.22.4" }, "devDependencies": { - "@biomejs/biome": "^1.7.3", + "@biomejs/biome": "^2.5.14", "@types/node": "^20.11.30", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", @@ -120,9 +120,10 @@ } }, "node_modules/@biomejs/biome": { - "version": "1.9.4", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.14.tgz", + "integrity": "sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw==", "dev": true, - "hasInstallScript": true, "license": "MIT OR Apache-2.0", "bin": { "biome": "bin/biome" @@ -135,20 +136,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "1.9.4", - "@biomejs/cli-darwin-x64": "1.9.4", - "@biomejs/cli-linux-arm64": "1.9.4", - "@biomejs/cli-linux-arm64-musl": "1.9.4", - "@biomejs/cli-linux-x64": "1.9.4", - "@biomejs/cli-linux-x64-musl": "1.9.4", - "@biomejs/cli-win32-arm64": "1.9.4", - "@biomejs/cli-win32-x64": "1.9.4" + "@biomejs/cli-darwin-arm64": "2.5.14", + "@biomejs/cli-darwin-x64": "2.5.14", + "@biomejs/cli-linux-arm64": "2.5.14", + "@biomejs/cli-linux-arm64-musl": "2.5.14", + "@biomejs/cli-linux-x64": "2.5.14", + "@biomejs/cli-linux-x64-musl": "2.5.14", + "@biomejs/cli-win32-arm64": "2.5.14", + "@biomejs/cli-win32-x64": "2.5.14" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", - "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.14.tgz", + "integrity": "sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ==", "cpu": [ "arm64" ], @@ -163,7 +164,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "1.9.4", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.14.tgz", + "integrity": "sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA==", "cpu": [ "x64" ], @@ -178,13 +181,16 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", - "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.14.tgz", + "integrity": "sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -195,13 +201,16 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", - "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.14.tgz", + "integrity": "sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -212,13 +221,16 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", - "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.14.tgz", + "integrity": "sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -229,13 +241,16 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", - "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.14.tgz", + "integrity": "sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -246,9 +261,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", - "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.14.tgz", + "integrity": "sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg==", "cpu": [ "arm64" ], @@ -263,9 +278,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", - "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.14.tgz", + "integrity": "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ==", "cpu": [ "x64" ], diff --git a/extensions/frontal-code-slack/package.json b/extensions/frontal-code-slack/package.json index 0bc405a..efd85a4 100644 --- a/extensions/frontal-code-slack/package.json +++ b/extensions/frontal-code-slack/package.json @@ -7,7 +7,7 @@ "scripts": { "clean": "rm -rf dist", "prebuild": "bun scripts/check-frontal-code-events.mjs || true", - "build": "bun build --target bun --minify --sourcemap src/index.ts --outdir dist", + "build": "bun build --target bun --minify src/index.ts --outdir dist", "build:dev": "bun build --target bun --sourcemap src/index.ts --outdir dist", "sync:frontal-code-events": "cargo run -p frontal-code-events --bin export-typescript -- src/generated/frontal-code-events.ts", "start": "node dist/index.js", @@ -53,7 +53,7 @@ "zod": "^3.22.4" }, "devDependencies": { - "@biomejs/biome": "^1.7.3", + "@biomejs/biome": "^2.5.14", "@types/node": "^20.11.30", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", diff --git a/extensions/frontal-code-slack/scripts/check-frontal-code-events.mjs b/extensions/frontal-code-slack/scripts/check-frontal-code-events.mjs index abc256d..f07bf96 100644 --- a/extensions/frontal-code-slack/scripts/check-frontal-code-events.mjs +++ b/extensions/frontal-code-slack/scripts/check-frontal-code-events.mjs @@ -5,29 +5,18 @@ import { join, resolve } from "node:path"; const extensionRoot = resolve(import.meta.dirname, ".."); const workspaceRoot = resolve(extensionRoot, "..", ".."); -const generatedFile = resolve( - extensionRoot, - "src/generated/frontal-code-events.ts" -); +const generatedFile = resolve(extensionRoot, "src/generated/frontal-code-events.ts"); const tempDir = mkdtempSync(join(tmpdir(), "frontal-code-events-check-")); const tempFile = join(tempDir, "frontal-code-events.ts"); try { execFileSync( "cargo", - [ - "run", - "-p", - "frontal-code-events", - "--bin", - "export-typescript", - "--", - tempFile, - ], + ["run", "-p", "frontal-code-events", "--bin", "export-typescript", "--", tempFile], { cwd: workspaceRoot, stdio: "pipe", - } + }, ); const expected = readFileSync(tempFile, "utf8"); @@ -38,7 +27,7 @@ try { "Generated Frontal Code event bindings are stale.", "Run `npm run sync:frontal-code-events` in extensions/frontal-code-slack and commit the updated file.", "", - ].join("\n") + ].join("\n"), ); process.exit(1); } diff --git a/extensions/frontal-code-slack/slack-manifest.json b/extensions/frontal-code-slack/slack-manifest.json index 595319c..a0e889d 100644 --- a/extensions/frontal-code-slack/slack-manifest.json +++ b/extensions/frontal-code-slack/slack-manifest.json @@ -27,9 +27,7 @@ "unfurl_domains": ["github.com", "gitlab.com", "bitbucket.org"] }, "oauth_config": { - "redirect_urls": [ - "https://your-frontal-code-server.com/slack/oauth/callback" - ], + "redirect_urls": ["https://your-frontal-code-server.com/slack/oauth/callback"], "scopes": { "user": [ "channels:read", diff --git a/extensions/frontal-code-slack/src/api-client.ts b/extensions/frontal-code-slack/src/api-client.ts index 697765c..09f1ec2 100644 --- a/extensions/frontal-code-slack/src/api-client.ts +++ b/extensions/frontal-code-slack/src/api-client.ts @@ -59,82 +59,78 @@ export class FrontalCodeApiClient { (error) => { logger.error("Frontal Code API request error", error as Error); return Promise.reject(error); - } + }, ); // Response interceptor for logging this.client.interceptors.response.use( (response) => { const startTime = response.config.headers?.["X-Start-Time"] - ? Number.parseInt(response.config.headers["X-Start-Time"] as string) + ? Number.parseInt(response.config.headers["X-Start-Time"] as string, 10) : 0; const duration = startTime ? Date.now() - startTime : 0; logApiCall( response.config.url || "", response.config.method?.toUpperCase() || "GET", duration, - true + true, ); return response; }, (error) => { const startTime = error.config?.headers?.["X-Start-Time"] - ? Number.parseInt(error.config.headers["X-Start-Time"] as string) + ? Number.parseInt(error.config.headers["X-Start-Time"] as string, 10) : 0; const duration = startTime ? Date.now() - startTime : 0; logApiCall( error.config?.url || "", error.config?.method?.toUpperCase() || "GET", duration, - false + false, ); return Promise.reject(error); - } + }, ); } - async submitPrompt( - request: FrontalCodePromptRequest - ): Promise { + async submitPrompt(request: FrontalCodePromptRequest): Promise { const startTime = Date.now(); try { - const response: AxiosResponse = - await this.client.post("/v1/prompt", request, { + const response: AxiosResponse = await this.client.post( + "/v1/prompt", + request, + { headers: new AxiosHeaders({ "X-Start-Time": startTime.toString(), }), - } as InternalAxiosRequestConfig); + } as InternalAxiosRequestConfig, + ); return response.data; } catch (error) { - logger.error( - "Failed to submit prompt to Frontal Code API", - error as Error - ); + logger.error("Failed to submit prompt to Frontal Code API", error as Error); throw new Error(`Frontal Code API error: ${(error as Error).message}`); } } - async runCliCommand( - request: FrontalCodeCliRequest - ): Promise { + async runCliCommand(request: FrontalCodeCliRequest): Promise { const startTime = Date.now(); try { - const response: AxiosResponse = - await this.client.post("/v1/cli/run", request, { + const response: AxiosResponse = await this.client.post( + "/v1/cli/run", + request, + { headers: new AxiosHeaders({ "X-Start-Time": startTime.toString(), }), - } as InternalAxiosRequestConfig); + } as InternalAxiosRequestConfig, + ); return response.data; } catch (error) { - logger.error( - "Failed to run CLI command via Frontal Code API", - error as Error - ); + logger.error("Failed to run CLI command via Frontal Code API", error as Error); throw new Error(`Frontal Code API error: ${(error as Error).message}`); } } @@ -143,19 +139,18 @@ export class FrontalCodeApiClient { const startTime = Date.now(); try { - const response: AxiosResponse = - await this.client.get("/v1/status", { + const response: AxiosResponse = await this.client.get( + "/v1/status", + { headers: new AxiosHeaders({ "X-Start-Time": startTime.toString(), }), - } as InternalAxiosRequestConfig); + } as InternalAxiosRequestConfig, + ); return response.data; } catch (error) { - logger.error( - "Failed to get status from Frontal Code API", - error as Error - ); + logger.error("Failed to get status from Frontal Code API", error as Error); throw new Error(`Frontal Code API error: ${(error as Error).message}`); } } @@ -164,19 +159,18 @@ export class FrontalCodeApiClient { const startTime = Date.now(); try { - const response: AxiosResponse = - await this.client.get("/v1/sandbox", { + const response: AxiosResponse = await this.client.get( + "/v1/sandbox", + { headers: new AxiosHeaders({ "X-Start-Time": startTime.toString(), }), - } as InternalAxiosRequestConfig); + } as InternalAxiosRequestConfig, + ); return response.data; } catch (error) { - logger.error( - "Failed to get sandbox status from Frontal Code API", - error as Error - ); + logger.error("Failed to get sandbox status from Frontal Code API", error as Error); throw new Error(`Frontal Code API error: ${(error as Error).message}`); } } @@ -197,10 +191,7 @@ export class FrontalCodeApiClient { return response.data; } catch (error) { - logger.error( - "Failed to get version from Frontal Code API", - error as Error - ); + logger.error("Failed to get version from Frontal Code API", error as Error); throw new Error(`Frontal Code API error: ${(error as Error).message}`); } } @@ -215,12 +206,12 @@ export class FrontalCodeApiClient { } } - async createTask( - request: FrontalCodeCreateTaskRequest - ): Promise { + async createTask(request: FrontalCodeCreateTaskRequest): Promise { try { - const response: AxiosResponse = - await this.client.post("/v1/tasks", request); + const response: AxiosResponse = await this.client.post( + "/v1/tasks", + request, + ); return response.data; } catch (error) { logger.error("Task creation failed", error as Error); @@ -230,9 +221,7 @@ export class FrontalCodeApiClient { async getTask(taskId: string): Promise { try { - const response: AxiosResponse = await this.client.get( - `/v1/tasks/${taskId}` - ); + const response: AxiosResponse = await this.client.get(`/v1/tasks/${taskId}`); return response.data; } catch (error) { logger.error("Task lookup failed", error as Error, { taskId }); @@ -240,16 +229,11 @@ export class FrontalCodeApiClient { } } - async listTasks( - query: FrontalCodeListTasksQuery = {} - ): Promise { + async listTasks(query: FrontalCodeListTasksQuery = {}): Promise { try { - const response: AxiosResponse = await this.client.get( - "/v1/tasks", - { - params: query, - } - ); + const response: AxiosResponse = await this.client.get("/v1/tasks", { + params: query, + }); return response.data; } catch (error) { logger.error("Task listing failed", error as Error, { ...query }); @@ -258,13 +242,15 @@ export class FrontalCodeApiClient { } async getOrphanPolicy( - query: FrontalCodeOrphanPolicyQuery = {} + query: FrontalCodeOrphanPolicyQuery = {}, ): Promise { try { - const response: AxiosResponse = - await this.client.get("/v1/policies/orphans", { + const response: AxiosResponse = await this.client.get( + "/v1/policies/orphans", + { params: query, - }); + }, + ); return response.data; } catch (error) { logger.error("Orphan policy lookup failed", error as Error, { ...query }); @@ -272,9 +258,7 @@ export class FrontalCodeApiClient { } } - async updateTaskContext( - request: FrontalCodeUpdateTaskContextRequest - ): Promise { + async updateTaskContext(request: FrontalCodeUpdateTaskContextRequest): Promise { try { const response: AxiosResponse = await this.client.post( `/v1/tasks/${request.taskId}/context`, @@ -284,7 +268,7 @@ export class FrontalCodeApiClient { channel_id: request.channel_id, thread_ts: request.thread_ts, approval_message_ts: request.approval_message_ts, - } + }, ); return response.data; } catch (error) { @@ -326,7 +310,7 @@ export class FrontalCodeApiClient { value?: string; userId: string; context: SlackBody; - } + }, ): Promise<{ blocks: SlackBlock[] }> { try { const response = await this.client.post( @@ -336,7 +320,7 @@ export class FrontalCodeApiClient { value: request.value, user_id: request.userId, context: request.context, - } + }, ); return response.data; } catch (error) { @@ -345,9 +329,7 @@ export class FrontalCodeApiClient { } } - async resolveTaskApproval( - request: FrontalCodeResolveApprovalRequest - ): Promise { + async resolveTaskApproval(request: FrontalCodeResolveApprovalRequest): Promise { try { const response: AxiosResponse = await this.client.post( `/v1/tasks/${request.taskId}/approval`, @@ -356,7 +338,7 @@ export class FrontalCodeApiClient { action: request.action, resolved_by: request.resolvedBy, reason: request.reason, - } + }, ); return response.data; } catch (error) { @@ -371,17 +353,14 @@ export class FrontalCodeApiClient { async sendConnectorEvent( connector: string, - request: { type: string; userId: string; data: unknown } + request: { type: string; userId: string; data: unknown }, ): Promise { try { - await this.client.post( - `/v1/connectors/${encodeURIComponent(connector)}/events`, - { - type: request.type, - user_id: request.userId, - data: request.data, - } - ); + await this.client.post(`/v1/connectors/${encodeURIComponent(connector)}/events`, { + type: request.type, + user_id: request.userId, + data: request.data, + }); } catch (error) { logger.error("Slack event processing failed", error as Error); throw error; diff --git a/extensions/frontal-code-slack/src/config.ts b/extensions/frontal-code-slack/src/config.ts index 20f3c94..21d798a 100644 --- a/extensions/frontal-code-slack/src/config.ts +++ b/extensions/frontal-code-slack/src/config.ts @@ -9,7 +9,7 @@ export interface Config { }; // Frontal Code API Configuration - frontal_code: { + frontalCode: { apiUrl: string; timeout: number; apiKey?: string; diff --git a/extensions/frontal-code-slack/src/env.ts b/extensions/frontal-code-slack/src/env.ts index 4d8624e..2f5b06c 100644 --- a/extensions/frontal-code-slack/src/env.ts +++ b/extensions/frontal-code-slack/src/env.ts @@ -25,12 +25,12 @@ export const env = createEnv({ .describe("Slack signing secret for request verification"), // Frontal Code API Configuration - FCODE_API_URL: z + FRONTAL_CODE_API_URL: z .string() .url() .default("http://frontal-code-api:8787") .describe("Frontal Code API base URL"), - FCODE_API_TIMEOUT: z.coerce + FRONTAL_CODE_API_TIMEOUT: z.coerce .number() .int() .min(1000) @@ -41,9 +41,7 @@ export const env = createEnv({ .string() .min(1) .optional() - .describe( - "Frontal Code API key for protected hosted control-plane routes" - ), + .describe("Frontal Code API key for protected hosted control-plane routes"), // Application Configuration NODE_ENV: z @@ -54,39 +52,17 @@ export const env = createEnv({ .enum(["error", "warn", "info", "http", "debug"]) .default("info") .describe("Logging level"), - PORT: z.coerce - .number() - .int() - .min(1000) - .max(65535) - .default(3000) - .describe("Server port"), + PORT: z.coerce.number().int().min(1000).max(65535).default(3000).describe("Server port"), // GitHub Configuration (Optional) - GITHUB_TOKEN: z - .string() - .min(1) - .optional() - .describe("GitHub personal access token"), + GITHUB_TOKEN: z.string().min(1).optional().describe("GitHub personal access token"), LINEAR_TOKEN: z.string().min(1).optional().describe("Linear API token"), - LINEAR_API_URL: z - .string() - .url() - .optional() - .describe("Linear GraphQL endpoint (optional)"), + LINEAR_API_URL: z.string().url().optional().describe("Linear GraphQL endpoint (optional)"), GRAPHITE_TOKEN: z.string().min(1).optional().describe("Graphite API token"), - GRAPHITE_API_URL: z - .string() - .url() - .optional() - .describe("Graphite API endpoint (optional)"), + GRAPHITE_API_URL: z.string().url().optional().describe("Graphite API endpoint (optional)"), // Sentry Configuration (Optional) - SENTRY_DSN: z - .string() - .url() - .optional() - .describe("Sentry DSN for error tracking"), + SENTRY_DSN: z.string().url().optional().describe("Sentry DSN for error tracking"), // Advanced Configuration MAX_CONCURRENT_TASKS: z.coerce @@ -131,9 +107,9 @@ export const env = createEnv({ SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN, SLACK_APP_TOKEN: process.env.SLACK_APP_TOKEN, SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET, - FCODE_API_URL: process.env.FCODE_API_URL, - FCODE_API_TIMEOUT: process.env.FCODE_API_TIMEOUT - ? Number(process.env.FCODE_API_TIMEOUT) + FRONTAL_CODE_API_URL: process.env.FRONTAL_CODE_API_URL, + FRONTAL_CODE_API_TIMEOUT: process.env.FRONTAL_CODE_API_TIMEOUT + ? Number(process.env.FRONTAL_CODE_API_TIMEOUT) : undefined, FRONTAL_SERVER_API_KEY: process.env.FRONTAL_SERVER_API_KEY, NODE_ENV: process.env.NODE_ENV, @@ -148,9 +124,7 @@ export const env = createEnv({ MAX_CONCURRENT_TASKS: process.env.MAX_CONCURRENT_TASKS ? Number(process.env.MAX_CONCURRENT_TASKS) : undefined, - TASK_TIMEOUT: process.env.TASK_TIMEOUT - ? Number(process.env.TASK_TIMEOUT) - : undefined, + TASK_TIMEOUT: process.env.TASK_TIMEOUT ? Number(process.env.TASK_TIMEOUT) : undefined, HEALTH_CHECK_INTERVAL: process.env.HEALTH_CHECK_INTERVAL ? Number(process.env.HEALTH_CHECK_INTERVAL) : undefined, @@ -169,9 +143,7 @@ export const env = createEnv({ onValidationError: (error: z.ZodError) => { console.error("Environment variable validation failed:"); console.error( - error.issues - .map((issue) => `${issue.path.join(".")}: ${issue.message}`) - .join("\n") + error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("\n"), ); throw new Error("Environment variable validation failed"); }, @@ -181,7 +153,7 @@ export const env = createEnv({ */ onInvalidAccess: (variable: string) => { throw new Error( - `Attempted to access server-side environment variable '${variable}' on the client` + `Attempted to access server-side environment variable '${variable}' on the client`, ); }, @@ -209,9 +181,9 @@ export function getEnvConfig() { appToken: env.SLACK_APP_TOKEN, signingSecret: env.SLACK_SIGNING_SECRET, }, - frontal_code: { - apiUrl: env.FCODE_API_URL, - timeout: env.FCODE_API_TIMEOUT, + frontalCode: { + apiUrl: env.FRONTAL_CODE_API_URL, + timeout: env.FRONTAL_CODE_API_TIMEOUT, apiKey: env.FRONTAL_SERVER_API_KEY, }, app: { @@ -247,6 +219,8 @@ export function getEnvConfig() { // Validation function for backward compatibility. // Deprecated: t3-oss/env-core handles validation automatically. // Kept as a no-op for any legacy importers; will be removed in a future release. -export function validateEnvConfig(): void {} +export function validateEnvConfig(): void { + console.log("Environment variables validated successfully"); +} export default env; diff --git a/extensions/frontal-code-slack/src/events-client.ts b/extensions/frontal-code-slack/src/events-client.ts index 63c836b..785e833 100644 --- a/extensions/frontal-code-slack/src/events-client.ts +++ b/extensions/frontal-code-slack/src/events-client.ts @@ -27,16 +27,14 @@ export class FrontalCodeEventsClient { "x-api-key": config.frontalCode.apiKey, }, } - : undefined + : undefined, ); this.ws.on("open", () => { logger.info("Connected to Frontal Code event stream", { url: this.url }); }); this.ws.on("message", (payload) => { try { - const event = JSON.parse( - payload.toString() - ) as FrontalCodeEventEnvelope; + const event = JSON.parse(payload.toString()) as FrontalCodeEventEnvelope; for (const handler of this.handlers) { handler(event); } @@ -83,9 +81,7 @@ export class FrontalCodeEventsClient { private buildWsUrl(): string { const base = config.frontalCode.apiUrl.replace(/\/+$/, ""); - const wsBase = base.replace(/^http/, (match) => - match === "https" ? "wss" : "ws" - ); + const wsBase = base.replace(/^http/, (match) => (match === "https" ? "wss" : "ws")); return `${wsBase}/v1/events/ws`; } } diff --git a/extensions/frontal-code-slack/src/frontal-code-events.ts b/extensions/frontal-code-slack/src/frontal-code-events.ts index 3809950..699c3e3 100644 --- a/extensions/frontal-code-slack/src/frontal-code-events.ts +++ b/extensions/frontal-code-slack/src/frontal-code-events.ts @@ -8,11 +8,9 @@ import type { type TrackedTaskHandler = ( event: FrontalCodeEventEnvelope, - task?: FrontalCodeTrackedTask + task?: FrontalCodeTrackedTask, ) => Promise | void; -type FrontalCodeEventsUrlBuilder = ( - query?: FrontalCodeEventStreamQuery -) => string; +type FrontalCodeEventsUrlBuilder = (query?: FrontalCodeEventStreamQuery) => string; type FrontalCodeEventsHeadersBuilder = () => Record | undefined; const RECENT_EVENTS_PER_TASK = 12; @@ -31,11 +29,10 @@ export class FrontalCodeEventsClient { private reconnectTimer?: NodeJS.Timeout; private shouldReconnect = false; private restartRequested = false; - private currentUrl?: string; constructor( urlBuilder: FrontalCodeEventsUrlBuilder, - headersBuilder: FrontalCodeEventsHeadersBuilder = () => undefined + headersBuilder: FrontalCodeEventsHeadersBuilder = () => undefined, ) { this.urlBuilder = urlBuilder; this.headersBuilder = headersBuilder; @@ -117,10 +114,7 @@ export class FrontalCodeEventsClient { if (this.restartRequested) { this.restartRequested = false; this.openSocket().catch((error) => { - logger.error( - "Frontal Code hosted events stream restart failed", - error as Error - ); + logger.error("Frontal Code hosted events stream restart failed", error as Error); this.scheduleReconnect(); }); } else { @@ -148,10 +142,7 @@ export class FrontalCodeEventsClient { return; } this.openSocket().catch((error) => { - logger.error( - "Frontal Code hosted events reconnect failed", - error as Error - ); + logger.error("Frontal Code hosted events reconnect failed", error as Error); this.scheduleReconnect(); }); }, RECONNECT_DELAY_MS); @@ -184,17 +175,14 @@ export class FrontalCodeEventsClient { private dispatchEvent( event: FrontalCodeEventEnvelope, - hintedTask?: FrontalCodeTrackedTask + hintedTask?: FrontalCodeTrackedTask, ): void { const taskId = event.task_id; if (!taskId) { return; } - const task = - hintedTask || - this.taskHints.get(taskId) || - this.readTrackedTaskFromEvent(event); + const task = hintedTask || this.taskHints.get(taskId) || this.readTrackedTaskFromEvent(event); if (task && !this.taskHints.has(taskId)) { this.taskHints.set(taskId, task); } @@ -215,20 +203,16 @@ export class FrontalCodeEventsClient { for (const handler of this.handlers) { void Promise.resolve(handler(event, task)).catch((error) => { - logger.error( - "Frontal Code hosted event handler failed", - error as Error, - { - event: event.event, - taskId, - } - ); + logger.error("Frontal Code hosted event handler failed", error as Error, { + event: event.event, + taskId, + }); }); } } private readTrackedTaskFromEvent( - event: FrontalCodeEventEnvelope + event: FrontalCodeEventEnvelope, ): FrontalCodeTrackedTask | undefined { const payload = event.payload; if (!payload) { diff --git a/extensions/frontal-code-slack/src/log.ts b/extensions/frontal-code-slack/src/log.ts index ece28bb..643e879 100644 --- a/extensions/frontal-code-slack/src/log.ts +++ b/extensions/frontal-code-slack/src/log.ts @@ -21,10 +21,7 @@ const winstonLogger = winston.createLogger({ winston.format.timestamp(), config.app.nodeEnv === "production" ? winston.format.json() - : winston.format.combine( - winston.format.colorize(), - winston.format.simple() - ) + : winston.format.combine(winston.format.colorize(), winston.format.simple()), ), transports: [new winston.transports.Console()], }); @@ -74,11 +71,7 @@ export const logger: Logger = { }; // Structured logging helpers -export const logTaskCreation = ( - taskId: string, - userId: string, - prompt: string -): void => { +export const logTaskCreation = (taskId: string, userId: string, prompt: string): void => { logger.info("Task created", { taskId, userId, @@ -88,11 +81,7 @@ export const logTaskCreation = ( }); }; -export const logTaskStatusChange = ( - taskId: string, - oldStatus: string, - newStatus: string -): void => { +export const logTaskStatusChange = (taskId: string, oldStatus: string, newStatus: string): void => { logger.info("Task status changed", { taskId, oldStatus, @@ -106,7 +95,7 @@ export const logApiCall = ( endpoint: string, method: string, duration: number, - success: boolean + success: boolean, ): void => { logger.info("API call", { endpoint, @@ -118,11 +107,7 @@ export const logApiCall = ( }); }; -export const logSlackEvent = ( - eventType: string, - userId: string, - channelId: string -): void => { +export const logSlackEvent = (eventType: string, userId: string, channelId: string): void => { logger.info("Slack event", { eventType, userId, @@ -132,11 +117,7 @@ export const logSlackEvent = ( }); }; -export const logUserAction = ( - userId: string, - userAction: string, - details?: LogMeta -): void => { +export const logUserAction = (userId: string, userAction: string, details?: LogMeta): void => { logger.info("User action", { userId, action: userAction, @@ -155,11 +136,7 @@ export const logSystemEvent = (event: string, details?: LogMeta): void => { }); }; -export const logSecurityEvent = ( - event: string, - userId: string, - details?: LogMeta -): void => { +export const logSecurityEvent = (event: string, userId: string, details?: LogMeta): void => { logger.warn("Security event", { event, userId, @@ -170,11 +147,7 @@ export const logSecurityEvent = ( }; // Performance logging -export const logPerformance = ( - operation: string, - duration: number, - details?: LogMeta -): void => { +export const logPerformance = (operation: string, duration: number, details?: LogMeta): void => { logger.info("Performance metric", { operation, duration, @@ -188,7 +161,7 @@ export const logPerformance = ( export const logHealthCheck = ( service: string, status: "healthy" | "unhealthy", - details?: LogMeta + details?: LogMeta, ): void => { if (status === "healthy") { logger.info(`Health check - ${service}`, { @@ -214,7 +187,7 @@ export const logDatabaseOperation = ( operation: string, table: string, duration?: number, - details?: LogMeta + details?: LogMeta, ): void => { logger.info("Database operation", { operation, @@ -231,7 +204,7 @@ export const logRedisOperation = ( operation: string, key?: string, duration?: number, - details?: LogMeta + details?: LogMeta, ): void => { logger.info("Redis operation", { operation, @@ -247,7 +220,7 @@ export const logRedisOperation = ( export const createErrorContext = ( userId?: string, taskId?: string, - channelId?: string + channelId?: string, ): LogMeta => { const context: LogMeta = {}; @@ -286,10 +259,7 @@ export class PerformanceTimer { } // Create a performance timer -export const startTimer = ( - operation: string, - details?: LogMeta -): PerformanceTimer => { +export const startTimer = (operation: string, details?: LogMeta): PerformanceTimer => { return new PerformanceTimer(operation, details); }; diff --git a/extensions/frontal-code-slack/src/slack.ts b/extensions/frontal-code-slack/src/slack.ts index 4bd7682..e0fd6e8 100644 --- a/extensions/frontal-code-slack/src/slack.ts +++ b/extensions/frontal-code-slack/src/slack.ts @@ -62,11 +62,11 @@ export class SlackInterface { this.frontalCodeApi = new FrontalCodeApiClient(); this.frontalCodeEvents = new FrontalCodeEventsClient( (query) => this.frontalCodeApi.getEventsWebSocketUrl(query), - () => this.frontalCodeApi.getEventsWebSocketHeaders() + () => this.frontalCodeApi.getEventsWebSocketHeaders(), ); this.frontalCodeEvents.onTrackedTaskEvent(async (event, task) => { - await this.handleFrontalCodeTaskEvent(event, task); - }); + await this.handleFrontalCodeTaskEvent(event, task); + }); this.setupEventHandlers(); } @@ -75,24 +75,18 @@ export class SlackInterface { this.app.message(async ({ message }) => { // Forward messages to Frontal Code AI const slackMessage = message as SlackMessage; - if ( - !slackMessage.text || - !slackMessage.user || - slackMessage.bot_id || - slackMessage.subtype - ) { + if (!slackMessage.text || !slackMessage.user || slackMessage.bot_id || slackMessage.subtype) { return; } const threadTs = slackMessage.thread_ts || slackMessage.ts; - const task = - await this.frontalCodeApi.createTask({ - prompt: slackMessage.text, - user_id: slackMessage.user, - channel_id: slackMessage.channel, - thread_ts: threadTs, - source: "slack", - }); + const task = await this.frontalCodeApi.createTask({ + prompt: slackMessage.text, + user_id: slackMessage.user, + channel_id: slackMessage.channel, + thread_ts: threadTs, + source: "slack", + }); await this.registerTask( task, @@ -102,7 +96,7 @@ export class SlackInterface { threadTs, userId: slackMessage.user, }, - slackMessage.text + slackMessage.text, ); }); @@ -111,8 +105,7 @@ export class SlackInterface { const policyQuery = this.parseOrphanPolicyCommand(command.text); if (policyQuery) { try { - const policy = - await this.frontalCodeApi.getOrphanPolicy(policyQuery); + const policy = await this.frontalCodeApi.getOrphanPolicy(policyQuery); await ack(this.buildOrphanPolicyCommandResponse(policy)); } catch (error) { await ack({ @@ -125,13 +118,12 @@ export class SlackInterface { await ack(); - const task = - await this.frontalCodeApi.createTask({ - prompt: command.text, - user_id: command.user_id, - channel_id: command.channel_id, - source: "slack", - }); + const task = await this.frontalCodeApi.createTask({ + prompt: command.text, + user_id: command.user_id, + channel_id: command.channel_id, + source: "slack", + }); await this.registerTask( task, @@ -140,17 +132,14 @@ export class SlackInterface { channelId: command.channel_id, userId: command.user_id, }, - command.text + command.text, ); }); // Handle interactions (buttons, modals, etc.) this.app.action({ action_id: /^.*$/ }, async ({ action, ack, body }) => { await ack(); - await this.handleSlackAction( - action as SlackAction, - body as unknown as SlackBody - ); + await this.handleSlackAction(action as SlackAction, body as unknown as SlackBody); }); // Handle events (user joins, reactions, etc.) @@ -158,10 +147,10 @@ export class SlackInterface { // Forward events to Frontal Code AI for processing const slackEvent = event as { type: string; user?: string }; await this.frontalCodeApi.sendConnectorEvent("slack", { - type: slackEvent.type, - userId: slackEvent.user || "", - data: event, - }); + type: slackEvent.type, + userId: slackEvent.user || "", + data: event, + }); }); } @@ -170,7 +159,7 @@ export class SlackInterface { try { await this.syncTrackedTasksFromFrontalCode(); await this.app.start(); - (await this.frontal) - codeEvents.connect(); + await this.frontalCodeEvents.connect(); logger.info("Slack WebSocket interface connected"); } catch (error) { logger.error("Failed to connect to Slack", error as Error); @@ -181,7 +170,7 @@ export class SlackInterface { // Disconnect WebSocket async disconnect(): Promise { try { - (await this.frontal) - codeEvents.disconnect(); + await this.frontalCodeEvents.disconnect(); await this.app.stop(); logger.info("Slack WebSocket interface disconnected"); } catch (error) { @@ -194,9 +183,7 @@ export class SlackInterface { async healthCheck(): Promise<{ slack: boolean; frontal_code: boolean }> { try { const slackConnected = - ( - this.app as unknown as { isListening?: () => boolean } - ).isListening?.() || false; + (this.app as unknown as { isListening?: () => boolean }).isListening?.() || false; const frontalCodeConnected = await this.frontalCodeApi.healthCheck(); return { @@ -215,7 +202,7 @@ export class SlackInterface { private async registerTask( task: FrontalCodeCreateTaskResponse, trackedTask: FrontalCodeTrackedTask, - prompt: string + prompt: string, ): Promise { const message = this.formatTaskCreatedMessage(task, prompt); const response = await this.app.client.chat.postMessage({ @@ -231,28 +218,22 @@ export class SlackInterface { this.upsertTrackedTask(nextTask); try { await this.frontalCodeApi.updateTaskContext({ - taskId: nextTask.taskId, - source: "slack", - user_id: nextTask.userId, - channel_id: nextTask.channelId, - thread_ts: nextTask.threadTs, - }); + taskId: nextTask.taskId, + source: "slack", + user_id: nextTask.userId, + channel_id: nextTask.channelId, + thread_ts: nextTask.threadTs, + }); } catch (error) { - logger.warn( - "Failed to persist Slack task thread anchor to Frontal Code", - { - taskId: nextTask.taskId, - error: (error as Error).message, - } - ); + logger.warn("Failed to persist Slack task thread anchor to Frontal Code", { + taskId: nextTask.taskId, + error: (error as Error).message, + }); } this.frontalCodeEvents.trackTask(nextTask); } - private async handleSlackAction( - action: SlackAction, - body: SlackBody - ): Promise { + private async handleSlackAction(action: SlackAction, body: SlackBody): Promise { if (this.isOrphanApprovalAction(action)) { await this.handleOrphanApprovalAction(action, body); return; @@ -262,13 +243,12 @@ export class SlackInterface { return; } - const response = - await this.frontalCodeApi.sendConnectorInteraction("slack", { - action: action.action_id, - value: action.value, - userId: body.user.id, - context: body, - }); + const response = await this.frontalCodeApi.sendConnectorInteraction("slack", { + action: action.action_id, + value: action.value, + userId: body.user.id, + context: body, + }); await this.app.client.chat.update({ channel: body.channel.id, @@ -293,29 +273,23 @@ export class SlackInterface { ); } - private async handleOrphanApprovalAction( - action: SlackAction, - body: SlackBody - ): Promise { + private async handleOrphanApprovalAction(action: SlackAction, body: SlackBody): Promise { const taskId = action.value; if (!taskId) { return; } - const actionName: FrontalCodeApprovalAction = action.action_id.endsWith( - ".retry" - ) + const actionName: FrontalCodeApprovalAction = action.action_id.endsWith(".retry") ? "retry" : "cancel"; - const approvalTs = - this.approvalMessageTsByTask.get(taskId) || body.message.ts; + const approvalTs = this.approvalMessageTsByTask.get(taskId) || body.message.ts; if (this.approvalResolved.has(taskId)) { await this.updateApprovalMessage( body.channel.id, approvalTs, `Approval for task ${taskId} was already resolved.`, - this.buildApprovalResolvedBlocks(taskId, "already_resolved") + this.buildApprovalResolvedBlocks(taskId, "already_resolved"), ); return; } @@ -325,7 +299,7 @@ export class SlackInterface { body.channel.id, approvalTs, `Approval for task ${taskId} is already being processed.`, - this.buildApprovalProcessingBlocks(taskId, actionName) + this.buildApprovalProcessingBlocks(taskId, actionName), ); return; } @@ -337,18 +311,17 @@ export class SlackInterface { body.channel.id, approvalTs, `Processing approval for task ${taskId}: ${actionName}.`, - this.buildApprovalProcessingBlocks(taskId, actionName) + this.buildApprovalProcessingBlocks(taskId, actionName), ); try { - const task = - await this.frontalCodeApi.resolveTaskApproval({ - taskId, - approvalKind: "orphaned_hosted_agent", - action: actionName, - resolvedBy: body.user.id, - reason: "resolved from Slack approval action", - }); + const task = await this.frontalCodeApi.resolveTaskApproval({ + taskId, + approvalKind: "orphaned_hosted_agent", + action: actionName, + resolvedBy: body.user.id, + reason: "resolved from Slack approval action", + }); this.approvalInFlight.delete(taskId); this.approvalResolved.add(taskId); @@ -361,8 +334,8 @@ export class SlackInterface { task.task_id, actionName, task.worker_status, - task.worker_id - ) + task.worker_id, + ), ); } catch (error) { this.approvalInFlight.delete(taskId); @@ -370,7 +343,7 @@ export class SlackInterface { body.channel.id, approvalTs, `Approval failed for task ${taskId}.`, - this.buildApprovalErrorBlocks(taskId, error as Error) + this.buildApprovalErrorBlocks(taskId, error as Error), ); throw error; } @@ -378,26 +351,23 @@ export class SlackInterface { private async handleGithubFollowupApprovalAction( action: SlackAction, - body: SlackBody + body: SlackBody, ): Promise { const taskId = action.value; if (!taskId) { return; } - const actionName: FrontalCodeApprovalAction = action.action_id.endsWith( - ".retry" - ) + const actionName: FrontalCodeApprovalAction = action.action_id.endsWith(".retry") ? "retry" : "ack"; - const approvalTs = - this.approvalMessageTsByTask.get(taskId) || body.message.ts; + const approvalTs = this.approvalMessageTsByTask.get(taskId) || body.message.ts; if (this.approvalResolved.has(taskId)) { await this.updateApprovalMessage( body.channel.id, approvalTs, `Follow-up for task ${taskId} was already resolved.`, - this.buildApprovalResolvedBlocks(taskId, "already_resolved") + this.buildApprovalResolvedBlocks(taskId, "already_resolved"), ); return; } @@ -407,27 +377,26 @@ export class SlackInterface { body.channel.id, approvalTs, `Follow-up for task ${taskId} is already being processed.`, - this.buildApprovalProcessingBlocks(taskId, actionName) + this.buildApprovalProcessingBlocks(taskId, actionName), ); return; } this.approvalInFlight.add(taskId); try { - const result = - await this.frontalCodeApi.resolveTaskApproval({ - taskId, - approvalKind: "github_review_followup", - action: actionName, - resolvedBy: body.user.name || body.user.id, - }); + const result = await this.frontalCodeApi.resolveTaskApproval({ + taskId, + approvalKind: "github_review_followup", + action: actionName, + resolvedBy: body.user.name || body.user.id, + }); this.approvalResolved.add(taskId); this.approvalInFlight.delete(taskId); await this.updateApprovalMessage( body.channel.id, approvalTs, `Follow-up resolved for task ${taskId}.`, - this.buildApprovalResolvedBlocks(taskId, actionName) + this.buildApprovalResolvedBlocks(taskId, actionName), ); this.trackTask(result); } catch (error) { @@ -436,7 +405,7 @@ export class SlackInterface { body.channel.id, approvalTs, `Follow-up for task ${taskId} failed: ${(error as Error).message}`, - this.buildApprovalErrorBlocks(taskId, error as Error) + this.buildApprovalErrorBlocks(taskId, error as Error), ); } } @@ -445,7 +414,7 @@ export class SlackInterface { channel: string, ts: string, text: string, - blocks: SlackBlock[] + blocks: SlackBlock[], ): Promise { await this.app.client.chat.update({ channel, @@ -457,7 +426,7 @@ export class SlackInterface { private async handleFrontalCodeTaskEvent( event: FrontalCodeEventEnvelope, - task?: FrontalCodeTrackedTask + task?: FrontalCodeTrackedTask, ): Promise { const resolvedTask = await this.resolveTrackedTaskForEvent(event, task); if (!resolvedTask) { @@ -472,10 +441,7 @@ export class SlackInterface { await this.postExternalStatusUpdates(event, message.text); - if ( - event.event === "approval.requested" && - this.approvalResolved.has(nextTask.taskId) - ) { + if (event.event === "approval.requested" && this.approvalResolved.has(nextTask.taskId)) { return; } @@ -508,21 +474,18 @@ export class SlackInterface { this.approvalMessageTsByTask.set(nextTask.taskId, response.ts as string); try { await this.frontalCodeApi.updateTaskContext({ - taskId: nextTask.taskId, - source: "slack", - user_id: nextTask.userId, - channel_id: nextTask.channelId, - thread_ts: nextTask.threadTs, - approval_message_ts: response.ts as string, - }); + taskId: nextTask.taskId, + source: "slack", + user_id: nextTask.userId, + channel_id: nextTask.channelId, + thread_ts: nextTask.threadTs, + approval_message_ts: response.ts as string, + }); } catch (error) { - logger.warn( - "Failed to persist Slack approval message linkage to Frontal Code", - { - taskId: nextTask.taskId, - error: (error as Error).message, - } - ); + logger.warn("Failed to persist Slack approval message linkage to Frontal Code", { + taskId: nextTask.taskId, + error: (error as Error).message, + }); } } @@ -533,7 +496,7 @@ export class SlackInterface { private async postExternalStatusUpdates( event: FrontalCodeEventEnvelope, - fallbackText: string + fallbackText: string, ): Promise { const payload = event.payload; if (!payload) { @@ -541,19 +504,14 @@ export class SlackInterface { } const hasLinear = - payload.linear_issue_id || - payload.linear_issue_identifier || - payload.linear_issue_url; + payload.linear_issue_id || payload.linear_issue_identifier || payload.linear_issue_url; const hasGraphite = - payload.graphite_stack_id || - payload.graphite_head_branch || - payload.graphite_base_branch; + payload.graphite_stack_id || payload.graphite_head_branch || payload.graphite_base_branch; if (!hasLinear && !hasGraphite) { return; } - const message = - this.buildExternalStatusMessage(event) ?? fallbackText ?? undefined; + const message = this.buildExternalStatusMessage(event) ?? fallbackText ?? undefined; if (!message) { return; } @@ -561,13 +519,13 @@ export class SlackInterface { if (hasLinear) { try { await this.frontalCodeApi.postLinearStatus({ - issueId: payload.linear_issue_id, - identifier: payload.linear_issue_identifier, - url: payload.linear_issue_url, - state: payload.linear_issue_state, - taskId: event.task_id, - message, - }); + issueId: payload.linear_issue_id, + identifier: payload.linear_issue_identifier, + url: payload.linear_issue_url, + state: payload.linear_issue_state, + taskId: event.task_id, + message, + }); } catch (error) { logger.warn("Failed to post Linear status", { error: (error as Error).message, @@ -578,12 +536,12 @@ export class SlackInterface { if (hasGraphite) { try { await this.frontalCodeApi.postGraphiteStatus({ - stackId: payload.graphite_stack_id, - headBranch: payload.graphite_head_branch, - baseBranch: payload.graphite_base_branch, - taskId: event.task_id, - message, - }); + stackId: payload.graphite_stack_id, + headBranch: payload.graphite_head_branch, + baseBranch: payload.graphite_base_branch, + taskId: event.task_id, + message, + }); } catch (error) { logger.warn("Failed to post Graphite status", { error: (error as Error).message, @@ -592,9 +550,7 @@ export class SlackInterface { } } - private buildExternalStatusMessage( - event: FrontalCodeEventEnvelope - ): string | undefined { + private buildExternalStatusMessage(event: FrontalCodeEventEnvelope): string | undefined { const summary = this.readEventTaskSummary(event); const syntheticTask: FrontalCodeTrackedTask = { taskId: event.task_id || summary?.work_item_id || "task", @@ -608,7 +564,7 @@ export class SlackInterface { private async resolveTrackedTaskForEvent( event: FrontalCodeEventEnvelope, - task?: FrontalCodeTrackedTask + task?: FrontalCodeTrackedTask, ): Promise { const taskId = event.task_id; if (!taskId) { @@ -637,39 +593,28 @@ export class SlackInterface { } this.syncDerivedApprovalState(snapshot); if (snapshot.approval_message_ts) { - this.approvalMessageTsByTask.set( - snapshotTask.taskId, - snapshot.approval_message_ts - ); + this.approvalMessageTsByTask.set(snapshotTask.taskId, snapshot.approval_message_ts); } this.upsertTrackedTask(snapshotTask); this.frontalCodeEvents.trackTask(snapshotTask); return snapshotTask; } catch (error) { - logger.warn( - "Failed to resolve Slack task routing from Frontal Code event", - { - taskId, - error: (error as Error).message, - } - ); + logger.warn("Failed to resolve Slack task routing from Frontal Code event", { + taskId, + error: (error as Error).message, + }); return undefined; } } - private formatTaskCreatedMessage( - task: FrontalCodeCreateTaskResponse, - prompt: string - ): string { + private formatTaskCreatedMessage(task: FrontalCodeCreateTaskResponse, prompt: string): string { const parts = [`Task created: ${task.task_id}`, `Prompt: ${prompt}`]; if (task.plan_kind) { parts.push(`Lane: ${task.plan_kind}`); } if (task.worker_status) { - parts.push( - `Worker: ${task.worker_status}${task.worker_id ? ` (${task.worker_id})` : ""}` - ); + parts.push(`Worker: ${task.worker_status}${task.worker_id ? ` (${task.worker_id})` : ""}`); } return parts.join("\n"); @@ -677,7 +622,7 @@ export class SlackInterface { private formatFrontalCodeEvent( event: FrontalCodeEventEnvelope, - task: FrontalCodeTrackedTask + task: FrontalCodeTrackedTask, ): SlackMessagePayload | null { const summary = this.readEventTaskSummary(event); const taskLabel = this.describeTask(task.taskId, summary); @@ -700,7 +645,7 @@ export class SlackInterface { const resultText = (payload?.result as string) || task.result || ""; const truncated = resultText && resultText.length > 2800 - ? resultText.substring(0, 2800) + "\n\n... (truncated)" + ? `${resultText.substring(0, 2800)}\n\n... (truncated)` : resultText || ""; const text = truncated ? `${taskLabel} completed:\n\n${truncated}` @@ -710,12 +655,7 @@ export class SlackInterface { case "approval.requested": return this.formatApprovalRequestedEvent(event, task); case "approval.resolved": - return this.formatApprovalResolvedEvent( - task, - taskLabel, - event, - summary - ); + return this.formatApprovalResolvedEvent(task, taskLabel, event, summary); case "memory.captured": return { text: `Memory captured for ${taskLabel.toLowerCase()}.` }; case "connector.event.received": @@ -727,10 +667,10 @@ export class SlackInterface { private formatConnectorEvent( taskLabel: string, - event: FrontalCodeEventEnvelope + event: FrontalCodeEventEnvelope, ): SlackMessagePayload | null { const payload = event.payload; - if (!payload || payload.connector !== "github") { + if (payload?.connector !== "github") { return null; } @@ -738,34 +678,39 @@ export class SlackInterface { const data = this.readConnectorEventData(payload.data); const actor = this.readConnectorString(data, "user_id", "sender_login"); const prNumber = - this.readConnectorNumber(data, "pr_number") ?? - this.readConnectorNumber(data, "number"); + this.readConnectorNumber(data, "pr_number") ?? this.readConnectorNumber(data, "number"); const htmlUrl = this.readConnectorString(data, "html_url"); const commentBody = this.readConnectorString(data, "comment_body"); const reviewBody = this.readConnectorString(data, "review_body"); const reviewState = this.readConnectorString(data, "review_state"); const prMerged = data?.pr_merged === true; + const prSuffix = prNumber ? ` #${prNumber}` : ""; + const prOnSuffix = prNumber ? ` on PR #${prNumber}` : ""; + const actorSuffix = actor ? ` by ${actor}` : ""; + const actorFromSuffix = actor ? ` from ${actor}` : ""; + const urlSuffix = htmlUrl ? ` ${htmlUrl}` : ""; + const reviewSuffix = reviewState ? ` (${reviewState.toLowerCase()})` : ""; switch (connectorType) { case "pull_request.synchronize": return { - text: `${taskLabel} received a GitHub PR update${prNumber ? ` (#${prNumber})` : ""}${actor ? ` from ${actor}` : ""}.${htmlUrl ? ` ${htmlUrl}` : ""}`, + text: `${taskLabel} received a GitHub PR update${prNumber ? ` (#${prNumber})` : ""}${actorFromSuffix}.${urlSuffix}`, }; case "pull_request.closed": return { - text: `${taskLabel} linked GitHub PR${prNumber ? ` #${prNumber}` : ""} was ${prMerged ? "merged" : "closed"}${actor ? ` by ${actor}` : ""}.${htmlUrl ? ` ${htmlUrl}` : ""}`, + text: `${taskLabel} linked GitHub PR${prSuffix} was ${prMerged ? "merged" : "closed"}${actorSuffix}.${urlSuffix}`, }; case "pull_request.reopened": return { - text: `${taskLabel} linked GitHub PR${prNumber ? ` #${prNumber}` : ""} was reopened${actor ? ` by ${actor}` : ""}.${htmlUrl ? ` ${htmlUrl}` : ""}`, + text: `${taskLabel} linked GitHub PR${prSuffix} was reopened${actorSuffix}.${urlSuffix}`, }; case "pull_request_review.submitted": return { - text: `${taskLabel} received a GitHub review${prNumber ? ` on PR #${prNumber}` : ""}${actor ? ` from ${actor}` : ""}${reviewState ? ` (${reviewState.toLowerCase()})` : ""}: ${reviewBody || "new review feedback"}`, + text: `${taskLabel} received a GitHub review${prOnSuffix}${actorFromSuffix}${reviewSuffix}: ${reviewBody || "new review feedback"}`, }; case "issue_comment.created": return { - text: `${taskLabel} received a GitHub comment${prNumber ? ` on PR #${prNumber}` : ""}${actor ? ` from ${actor}` : ""}: ${commentBody || "new review feedback"}`, + text: `${taskLabel} received a GitHub comment${prOnSuffix}${actorFromSuffix}: ${commentBody || "new review feedback"}`, }; default: return null; @@ -775,10 +720,9 @@ export class SlackInterface { private formatTaskRoutedEvent( taskLabel: string, event: FrontalCodeEventEnvelope, - summary: FrontalCodeEventTaskSummary + summary: FrontalCodeEventTaskSummary, ): SlackMessagePayload { - const planKind = - summary.plan_kind || event.payload?.plan_kind || "assigned"; + const planKind = summary.plan_kind || event.payload?.plan_kind || "assigned"; const laneCount = event.payload?.lane_count; return { text: `${taskLabel} routed to ${planKind}${laneCount ? ` (${laneCount} lanes)` : ""}.`, @@ -787,11 +731,9 @@ export class SlackInterface { private formatTaskCancelledEvent( taskLabel: string, - event: FrontalCodeEventEnvelope + event: FrontalCodeEventEnvelope, ): SlackMessagePayload { - const policyText = this.describeOrphanPolicy( - this.readOrphanPolicy(event.payload) - ); + const policyText = this.describeOrphanPolicy(this.readOrphanPolicy(event.payload)); return { text: `${taskLabel} was cancelled.${policyText ? ` ${policyText}` : ""}`, }; @@ -800,11 +742,10 @@ export class SlackInterface { private formatLaneStartedEvent( taskLabel: string, event: FrontalCodeEventEnvelope, - summary: FrontalCodeEventTaskSummary + summary: FrontalCodeEventTaskSummary, ): SlackMessagePayload { const role = event.payload?.role || "lane"; - const workerStatus = - summary.worker_status || event.payload?.worker_status || event.status; + const workerStatus = summary.worker_status || event.payload?.worker_status || event.status; const workerId = summary.worker_id || event.payload?.worker_id; return { text: `${taskLabel} ${role} started with worker ${workerStatus}${workerId ? ` (${workerId})` : ""}.`, @@ -814,10 +755,9 @@ export class SlackInterface { private formatLaneFailedEvent( taskLabel: string, event: FrontalCodeEventEnvelope, - summary: FrontalCodeEventTaskSummary + summary: FrontalCodeEventTaskSummary, ): SlackMessagePayload { - const error = - summary.error || event.payload?.error || "lane execution failed"; + const error = summary.error || event.payload?.error || "lane execution failed"; return { text: `${taskLabel} failed: ${error}`, }; @@ -825,13 +765,10 @@ export class SlackInterface { private formatLaneBlockedEvent( taskLabel: string, - event: FrontalCodeEventEnvelope + event: FrontalCodeEventEnvelope, ): SlackMessagePayload { - const reason = - event.payload?.reason || event.payload?.detail || "waiting for input"; - const policyText = this.describeOrphanPolicy( - this.readOrphanPolicy(event.payload) - ); + const reason = event.payload?.reason || event.payload?.detail || "waiting for input"; + const policyText = this.describeOrphanPolicy(this.readOrphanPolicy(event.payload)); return { text: `${taskLabel} is blocked: ${reason}${policyText ? ` ${policyText}` : ""}`, }; @@ -841,7 +778,7 @@ export class SlackInterface { task: FrontalCodeTrackedTask, taskLabel: string, event: FrontalCodeEventEnvelope, - summary: FrontalCodeEventTaskSummary + summary: FrontalCodeEventTaskSummary, ): SlackMessagePayload { const action = event.payload?.action || "updated"; const workerStatus = summary.worker_status || event.payload?.worker_status; @@ -854,27 +791,20 @@ export class SlackInterface { } return { text: `Approval resolved for ${taskLabel.toLowerCase()}: ${action}.`, - blocks: this.buildApprovalResolvedBlocks( - task.taskId, - action, - workerStatus, - workerId - ), + blocks: this.buildApprovalResolvedBlocks(task.taskId, action, workerStatus, workerId), }; } private formatApprovalRequestedEvent( event: FrontalCodeEventEnvelope, - task: FrontalCodeTrackedTask + task: FrontalCodeTrackedTask, ): SlackMessagePayload { const summary = this.readEventTaskSummary(event); const payload = event.payload; const taskLabel = this.describeTask(task.taskId, summary); const approvalKind = payload?.approval_kind; const reason = payload?.reason || "Task requires approval."; - const policyText = this.describeOrphanPolicy( - this.readOrphanPolicy(payload) - ); + const policyText = this.describeOrphanPolicy(this.readOrphanPolicy(payload)); if (approvalKind === "orphaned_hosted_agent") { return { @@ -964,9 +894,7 @@ export class SlackInterface { }; } - private readEventTaskSummary( - event: FrontalCodeEventEnvelope - ): FrontalCodeEventTaskSummary { + private readEventTaskSummary(event: FrontalCodeEventEnvelope): FrontalCodeEventTaskSummary { const payload = event.payload; if (!payload) { return {}; @@ -1001,7 +929,7 @@ export class SlackInterface { } private readConnectorEventData( - data: FrontalCodeEventPayload["data"] + data: FrontalCodeEventPayload["data"], ): Record | undefined { return data && typeof data === "object" && !Array.isArray(data) ? (data as Record) @@ -1036,7 +964,7 @@ export class SlackInterface { private hydrateTrackedTaskFromEvent( task: FrontalCodeTrackedTask, - event: FrontalCodeEventEnvelope + event: FrontalCodeEventEnvelope, ): FrontalCodeTrackedTask { const summary = this.readEventTaskSummary(event); const nextChannelId = summary.channel_id || task.channelId; @@ -1065,7 +993,7 @@ export class SlackInterface { private readTrackedTaskFromEventSummary( taskId: string, - event: FrontalCodeEventEnvelope + event: FrontalCodeEventEnvelope, ): FrontalCodeTrackedTask | undefined { const summary = this.readEventTaskSummary(event); if (!summary.channel_id) { @@ -1080,19 +1008,14 @@ export class SlackInterface { }; } - private describeTask( - taskId: string, - summary: FrontalCodeEventTaskSummary - ): string { + private describeTask(taskId: string, summary: FrontalCodeEventTaskSummary): string { if (summary.repository) { return `Task ${taskId} (${summary.repository})`; } return `Task ${taskId}`; } - private parseOrphanPolicyCommand( - text: string - ): FrontalCodeOrphanPolicyQuery | null { + private parseOrphanPolicyCommand(text: string): FrontalCodeOrphanPolicyQuery | null { const trimmed = text.trim(); if (!trimmed) { return null; @@ -1124,22 +1047,16 @@ export class SlackInterface { return query; } - private buildOrphanPolicyCommandResponse( - policy: FrontalCodeOrphanPolicyResponse - ): { + private buildOrphanPolicyCommandResponse(policy: FrontalCodeOrphanPolicyResponse): { response_type: "ephemeral"; text: string; blocks: SlackBlock[]; } { const preview = policy.preview ? [ - policy.preview.repository - ? `repo=${policy.preview.repository}` - : undefined, + policy.preview.repository ? `repo=${policy.preview.repository}` : undefined, policy.preview.source ? `source=${policy.preview.source}` : undefined, - policy.preview.priority - ? `priority=${policy.preview.priority}` - : undefined, + policy.preview.priority ? `priority=${policy.preview.priority}` : undefined, ] .filter((value): value is string => Boolean(value)) .join(", ") @@ -1148,8 +1065,7 @@ export class SlackInterface { const ruleLines = this.formatOrphanPolicyRuleLines(policy); const effectivePolicyText = - this.describeOrphanPolicy(policy.effective_policy) || - "Policy unavailable."; + this.describeOrphanPolicy(policy.effective_policy) || "Policy unavailable."; const defaultPolicyText = this.describeOrphanPolicy(policy.default_policy) || "Policy unavailable."; @@ -1176,14 +1092,12 @@ export class SlackInterface { } private readOrphanPolicy( - payload?: FrontalCodeEventPayload + payload?: FrontalCodeEventPayload, ): FrontalCodeAppliedOrphanPolicy | undefined { return payload?.orphan_policy; } - private describeOrphanPolicy( - policy?: FrontalCodeAppliedOrphanPolicy - ): string | undefined { + private describeOrphanPolicy(policy?: FrontalCodeAppliedOrphanPolicy): string | undefined { if (!policy) { return undefined; } @@ -1194,9 +1108,7 @@ export class SlackInterface { policy.match_priority ? `priority=${policy.match_priority}` : undefined, ].filter((value): value is string => Boolean(value)); const policyScope = - selectors.length > 0 - ? `${policy.source} (${selectors.join(", ")})` - : policy.source; + selectors.length > 0 ? `${policy.source} (${selectors.join(", ")})` : policy.source; const steps = [ `approval ${policy.approval_delay_secs}s`, policy.auto_retry_after_secs !== undefined @@ -1210,9 +1122,7 @@ export class SlackInterface { return `Policy: ${policyScope}; ${steps.join(", ")}.`; } - private formatOrphanPolicyRuleLines( - policy: FrontalCodeOrphanPolicyResponse - ): string[] { + private formatOrphanPolicyRuleLines(policy: FrontalCodeOrphanPolicyResponse): string[] { if (policy.configured_rules.length === 0) { return ["No scoped rules configured."]; } @@ -1224,7 +1134,7 @@ export class SlackInterface { private formatOrphanPolicyRuleLine( rule: FrontalCodeOrphanPolicyResponse["configured_rules"][number], - index: number + index: number, ): string { const selectors = [ rule.repository ? `repo=${rule.repository}` : undefined, @@ -1234,12 +1144,8 @@ export class SlackInterface { .filter((value): value is string => Boolean(value)) .join(", "); const timing = [ - rule.approval_delay_secs !== undefined - ? `approval ${rule.approval_delay_secs}s` - : undefined, - rule.auto_retry_after_secs !== undefined - ? `retry ${rule.auto_retry_after_secs}s` - : undefined, + rule.approval_delay_secs !== undefined ? `approval ${rule.approval_delay_secs}s` : undefined, + rule.auto_retry_after_secs !== undefined ? `retry ${rule.auto_retry_after_secs}s` : undefined, rule.auto_cancel_after_secs !== undefined ? `cancel ${rule.auto_cancel_after_secs}s` : undefined, @@ -1252,7 +1158,7 @@ export class SlackInterface { private buildApprovalProcessingBlocks( taskId: string, - action: FrontalCodeApprovalAction + action: FrontalCodeApprovalAction, ): SlackBlock[] { return [ { @@ -1269,7 +1175,7 @@ export class SlackInterface { taskId: string, action: string, workerStatus?: string, - workerId?: string + workerId?: string, ): SlackBlock[] { const detail = workerStatus && workerId @@ -1330,11 +1236,10 @@ export class SlackInterface { private async syncTrackedTasksFromFrontalCode(): Promise { try { - const activeTasks = - await this.frontalCodeApi.listTasks({ - source: "slack", - status: "pending,running", - }); + const activeTasks = await this.frontalCodeApi.listTasks({ + source: "slack", + status: "pending,running", + }); let merged = 0; for (const task of activeTasks) { @@ -1348,10 +1253,7 @@ export class SlackInterface { } this.upsertTrackedTask(trackedTask); if (task.approval_message_ts) { - this.approvalMessageTsByTask.set( - trackedTask.taskId, - task.approval_message_ts - ); + this.approvalMessageTsByTask.set(trackedTask.taskId, task.approval_message_ts); } this.syncDerivedApprovalState(task); this.frontalCodeEvents.trackTask(trackedTask); @@ -1401,9 +1303,7 @@ export class SlackInterface { this.trackedTasks.set(task.taskId, mergedTask); } - private sanitizeTrackedTask( - task: FrontalCodeTrackedTask - ): FrontalCodeTrackedTask { + private sanitizeTrackedTask(task: FrontalCodeTrackedTask): FrontalCodeTrackedTask { return { taskId: task.taskId, channelId: task.channelId, @@ -1424,7 +1324,7 @@ export class SlackInterface { this.approvalResolved.delete(task.task_id); } - private async cleanupTaskState(taskId: string): Promise { + private cleanupTaskState(taskId: string): void { this.trackedTasks.delete(taskId); this.frontalCodeEvents.untrackTask(taskId); this.approvalMessageTsByTask.delete(taskId); diff --git a/extensions/frontal-code-slack/src/types.ts b/extensions/frontal-code-slack/src/types.ts index e3af130..a235a59 100644 --- a/extensions/frontal-code-slack/src/types.ts +++ b/extensions/frontal-code-slack/src/types.ts @@ -147,7 +147,7 @@ export interface SlackConversationContext { } // ============================================================================ -// FCODE TYPES +// FRONTAL CODE TYPES // ============================================================================ export interface FrontalCodePromptRequest { @@ -326,18 +326,15 @@ export interface FrontalCodeTerminalEventPayload { derived_state?: string; } -export type FrontalCodeEventTaskSummary = - FrontalCodeGeneratedHostedTaskEventSummary & { - task_status?: FrontalCodeTask["status"]; - result?: string; - error?: string; - }; +export type FrontalCodeEventTaskSummary = FrontalCodeGeneratedHostedTaskEventSummary & { + task_status?: FrontalCodeTask["status"]; + result?: string; + error?: string; +}; -export type FrontalCodeAppliedOrphanPolicy = - FrontalCodeGeneratedAppliedOrphanPolicy; +export type FrontalCodeAppliedOrphanPolicy = FrontalCodeGeneratedAppliedOrphanPolicy; -export type FrontalCodeConnectorEventPayload = - FrontalCodeGeneratedConnectorEventPayload; +export type FrontalCodeConnectorEventPayload = FrontalCodeGeneratedConnectorEventPayload; export type FrontalCodeEventPayload = FrontalCodeGeneratedEventPayload; @@ -402,19 +399,13 @@ export interface FrontalCodeResolveApprovalRequest { export interface SlackTask { id: string; slack_task_id: string; - frontal-code_task_id?: string -user_id: string; -status: -| "pending" - | "submitted" - | "running" - | "completed" - | "failed" - | "cancelled" -request: FrontalCodePromptRequest; -response?: FrontalCodeCliResponse; -created_at: Date; -updated_at: Date; + frontal_code_task_id?: string; + user_id: string; + status: "pending" | "submitted" | "running" | "completed" | "failed" | "cancelled"; + request: FrontalCodePromptRequest; + response?: FrontalCodeCliResponse; + created_at: Date; + updated_at: Date; } export interface SlackUserPreferences { diff --git a/extensions/frontal-code-slack/tests/api-client-interceptors.test.ts b/extensions/frontal-code-slack/tests/api-client-interceptors.test.ts index ddc5fc4..b3a6be6 100644 --- a/extensions/frontal-code-slack/tests/api-client-interceptors.test.ts +++ b/extensions/frontal-code-slack/tests/api-client-interceptors.test.ts @@ -25,13 +25,11 @@ const interceptorState = vi.hoisted(() => { }), }, response: { - use: vi.fn( - (onFulfilled: ResponseSuccess, onRejected: ResponseError) => { - captured.responseSuccess = onFulfilled; - captured.responseError = onRejected; - return 0; - } - ), + use: vi.fn((onFulfilled: ResponseSuccess, onRejected: ResponseError) => { + captured.responseSuccess = onFulfilled; + captured.responseError = onRejected; + return 0; + }), }, }, }; @@ -69,7 +67,7 @@ vi.mock("../src/log", () => ({ vi.mock("../src/config", () => ({ config: { - frontal_code: { + frontalCode: { apiUrl: "http://localhost:8787", timeout: 30_000, }, @@ -108,26 +106,18 @@ describe("FrontalCodeApiClient interceptors", () => { }; expect(interceptorState.captured.requestSuccess?.(config)).toBe(config); - expect(logState.logger.debug).toHaveBeenCalledWith( - "Frontal Code API request", - { - method: "post", - url: "/v1/tasks", - } - ); + expect(logState.logger.debug).toHaveBeenCalledWith("Frontal Code API request", { + method: "post", + url: "/v1/tasks", + }); }); it("logs and rethrows request setup failures from the request interceptor", async () => { new FrontalCodeApiClient(); const error = new Error("bad config"); - await expect(interceptorState.captured.requestError?.(error)).rejects.toBe( - error - ); - expect(logState.logger.error).toHaveBeenCalledWith( - "Frontal Code API request error", - error - ); + await expect(interceptorState.captured.requestError?.(error)).rejects.toBe(error); + expect(logState.logger.error).toHaveBeenCalledWith("Frontal Code API request error", error); }); it("logs successful API responses with computed duration from the response interceptor", () => { @@ -143,15 +133,8 @@ describe("FrontalCodeApiClient interceptors", () => { }, }; - expect(interceptorState.captured.responseSuccess?.(response)).toBe( - response - ); - expect(logState.logApiCall).toHaveBeenCalledWith( - "/v1/tasks", - "GET", - 1500, - true - ); + expect(interceptorState.captured.responseSuccess?.(response)).toBe(response); + expect(logState.logApiCall).toHaveBeenCalledWith("/v1/tasks", "GET", 1500, true); }); it("falls back to zero duration when the response success path has no start-time header", () => { @@ -164,15 +147,8 @@ describe("FrontalCodeApiClient interceptors", () => { }, }; - expect(interceptorState.captured.responseSuccess?.(response)).toBe( - response - ); - expect(logState.logApiCall).toHaveBeenCalledWith( - "/v1/tasks", - "GET", - 0, - true - ); + expect(interceptorState.captured.responseSuccess?.(response)).toBe(response); + expect(logState.logApiCall).toHaveBeenCalledWith("/v1/tasks", "GET", 0, true); }); it("falls back to empty url and GET when the response success path lacks method and url", () => { @@ -186,9 +162,7 @@ describe("FrontalCodeApiClient interceptors", () => { }; vi.spyOn(Date, "now").mockReturnValue(2_500); - expect(interceptorState.captured.responseSuccess?.(response)).toBe( - response - ); + expect(interceptorState.captured.responseSuccess?.(response)).toBe(response); expect(logState.logApiCall).toHaveBeenCalledWith("", "GET", 1500, true); }); @@ -205,24 +179,15 @@ describe("FrontalCodeApiClient interceptors", () => { }, }); - await expect(interceptorState.captured.responseError?.(error)).rejects.toBe( - error - ); - expect(logState.logApiCall).toHaveBeenCalledWith( - "/v1/tasks/123", - "POST", - 1500, - false - ); + await expect(interceptorState.captured.responseError?.(error)).rejects.toBe(error); + expect(logState.logApiCall).toHaveBeenCalledWith("/v1/tasks/123", "POST", 1500, false); }); it("falls back to empty url, GET, and zero duration when the response error lacks config", async () => { new FrontalCodeApiClient(); const error = new Error("down"); - await expect(interceptorState.captured.responseError?.(error)).rejects.toBe( - error - ); + await expect(interceptorState.captured.responseError?.(error)).rejects.toBe(error); expect(logState.logApiCall).toHaveBeenCalledWith("", "GET", 0, false); }); }); diff --git a/extensions/frontal-code-slack/tests/api-client.test.ts b/extensions/frontal-code-slack/tests/api-client.test.ts index 6cea930..0065894 100644 --- a/extensions/frontal-code-slack/tests/api-client.test.ts +++ b/extensions/frontal-code-slack/tests/api-client.test.ts @@ -12,19 +12,14 @@ interface MockHttpClient { post: ReturnType; } -function installMockHttpClient( - client: FrontalCodeApiClient, - mockHttpClient: MockHttpClient -): void { +function installMockHttpClient(client: FrontalCodeApiClient, mockHttpClient: MockHttpClient): void { Object.defineProperty(client, "client", { value: mockHttpClient, configurable: true, }); } -function createTaskSnapshot( - overrides: Partial = {} -): FrontalCodeTask { +function createTaskSnapshot(overrides: Partial = {}): FrontalCodeTask { return { task_id: "task-123", prompt: "Investigate flaky test", @@ -58,7 +53,7 @@ describe("FrontalCodeApiClient", () => { }); expect(url).toBe( - "ws://localhost:8787/v1/events/ws?source=slack&status=running&repository=frontal-code%2Fslack&limit=25" + "ws://localhost:8787/v1/events/ws?source=slack&status=running&repository=frontal-code%2Fslack&limit=25", ); }); @@ -70,7 +65,7 @@ describe("FrontalCodeApiClient", () => { }); expect(secureClient.getEventsWebSocketUrl({ source: "slack" })).toBe( - "wss://frontal-code.example.com/v1/events/ws?source=slack" + "wss://frontal-code.example.com/v1/events/ws?source=slack", ); }); @@ -97,7 +92,7 @@ describe("FrontalCodeApiClient", () => { }, expect.objectContaining({ headers: expect.anything(), - }) + }), ); expect(response).toEqual(responseBody); }); @@ -108,7 +103,7 @@ describe("FrontalCodeApiClient", () => { await expect( client.submitPrompt({ prompt: "Fix the failing Slack test", - }) + }), ).rejects.toThrow("Frontal Code API error: timeout"); }); @@ -135,7 +130,7 @@ describe("FrontalCodeApiClient", () => { }, expect.objectContaining({ headers: expect.anything(), - }) + }), ); expect(response).toEqual(responseBody); }); @@ -146,7 +141,7 @@ describe("FrontalCodeApiClient", () => { await expect( client.runCliCommand({ args: ["status"], - }) + }), ).rejects.toThrow("Frontal Code API error: cli timeout"); }); @@ -181,14 +176,14 @@ describe("FrontalCodeApiClient", () => { "/v1/status", expect.objectContaining({ headers: expect.anything(), - }) + }), ); expect(mockHttpClient.get).toHaveBeenNthCalledWith( 2, "/v1/version", expect.objectContaining({ headers: expect.anything(), - }) + }), ); }); @@ -196,7 +191,7 @@ describe("FrontalCodeApiClient", () => { mockHttpClient.get.mockRejectedValue(new Error("version unavailable")); await expect(client.getVersion()).rejects.toThrow( - "Frontal Code API error: version unavailable" + "Frontal Code API error: version unavailable", ); }); @@ -206,15 +201,9 @@ describe("FrontalCodeApiClient", () => { .mockRejectedValueOnce(new Error("sandbox down")) .mockRejectedValueOnce(new Error("version down")); - await expect(client.getStatus()).rejects.toThrow( - "Frontal Code API error: status down" - ); - await expect(client.getSandboxStatus()).rejects.toThrow( - "Frontal Code API error: sandbox down" - ); - await expect(client.getVersion()).rejects.toThrow( - "Frontal Code API error: version down" - ); + await expect(client.getStatus()).rejects.toThrow("Frontal Code API error: status down"); + await expect(client.getSandboxStatus()).rejects.toThrow("Frontal Code API error: sandbox down"); + await expect(client.getVersion()).rejects.toThrow("Frontal Code API error: version down"); }); it("creates tasks and resolves approvals through the hosted task routes", async () => { @@ -239,7 +228,7 @@ describe("FrontalCodeApiClient", () => { prompt: "Investigate flaky test", source: "slack", channel_id: "C123", - }) + }), ).resolves.toEqual(created); await expect( @@ -249,7 +238,7 @@ describe("FrontalCodeApiClient", () => { action: "cancel", resolvedBy: "U123", reason: "User approved cancellation", - }) + }), ).resolves.toEqual(approved); expect(mockHttpClient.post).toHaveBeenNthCalledWith(1, "/v1/tasks", { @@ -257,16 +246,12 @@ describe("FrontalCodeApiClient", () => { source: "slack", channel_id: "C123", }); - expect(mockHttpClient.post).toHaveBeenNthCalledWith( - 2, - "/v1/tasks/task-123/approval", - { - approval_kind: "orphaned_hosted_agent", - action: "cancel", - resolved_by: "U123", - reason: "User approved cancellation", - } - ); + expect(mockHttpClient.post).toHaveBeenNthCalledWith(2, "/v1/tasks/task-123/approval", { + approval_kind: "orphaned_hosted_agent", + action: "cancel", + resolved_by: "U123", + reason: "User approved cancellation", + }); }); it("looks up a single task and the effective orphan policy", async () => { @@ -297,20 +282,16 @@ describe("FrontalCodeApiClient", () => { .mockResolvedValueOnce({ data: policy }); await expect(client.getTask("task-123")).resolves.toEqual(task); - await expect( - client.getOrphanPolicy({ repository: "frontal-code/slack" }) - ).resolves.toEqual(policy); + await expect(client.getOrphanPolicy({ repository: "frontal-code/slack" })).resolves.toEqual( + policy, + ); expect(mockHttpClient.get).toHaveBeenNthCalledWith(1, "/v1/tasks/task-123"); - expect(mockHttpClient.get).toHaveBeenNthCalledWith( - 2, - "/v1/policies/orphans", - { - params: { - repository: "frontal-code/slack", - }, - } - ); + expect(mockHttpClient.get).toHaveBeenNthCalledWith(2, "/v1/policies/orphans", { + params: { + repository: "frontal-code/slack", + }, + }); }); it("passes list task filters through to the hosted tasks endpoint", async () => { @@ -354,16 +335,13 @@ describe("FrontalCodeApiClient", () => { const response = await client.updateTaskContext(request); - expect(mockHttpClient.post).toHaveBeenCalledWith( - "/v1/tasks/task-123/context", - { - source: "slack", - user_id: "U123", - channel_id: "C123", - thread_ts: "1710000000.100", - approval_message_ts: "1710000000.200", - } - ); + expect(mockHttpClient.post).toHaveBeenCalledWith("/v1/tasks/task-123/context", { + source: "slack", + user_id: "U123", + channel_id: "C123", + thread_ts: "1710000000.100", + approval_message_ts: "1710000000.200", + }); expect(response).toEqual(task); }); @@ -391,15 +369,12 @@ describe("FrontalCodeApiClient", () => { context: request, }); - expect(mockHttpClient.post).toHaveBeenCalledWith( - "/v1/connectors/slack/interactions", - { - action: "orphaned_hosted_agent.retry", - value: "task-123", - user_id: "U123", - context: request, - } - ); + expect(mockHttpClient.post).toHaveBeenCalledWith("/v1/connectors/slack/interactions", { + action: "orphaned_hosted_agent.retry", + value: "task-123", + user_id: "U123", + context: request, + }); expect(response).toEqual({ blocks }); }); @@ -412,14 +387,11 @@ describe("FrontalCodeApiClient", () => { data: { reaction: "eyes" }, }); - expect(mockHttpClient.post).toHaveBeenCalledWith( - "/v1/connectors/slack/events", - { - type: "reaction_added", - user_id: "U123", - data: { reaction: "eyes" }, - } - ); + expect(mockHttpClient.post).toHaveBeenCalledWith("/v1/connectors/slack/events", { + type: "reaction_added", + user_id: "U123", + data: { reaction: "eyes" }, + }); }); it("returns false when the hosted health check fails", async () => { @@ -465,16 +437,14 @@ describe("FrontalCodeApiClient", () => { "/v1/sandbox", expect.objectContaining({ headers: expect.anything(), - }) + }), ); expect(response).toEqual(sandbox); }); it("rethrows checkSandboxStatus failures after logging them", async () => { const failure = new Error("sandbox unavailable"); - const sandboxSpy = vi - .spyOn(client, "getSandboxStatus") - .mockRejectedValue(failure); + const sandboxSpy = vi.spyOn(client, "getSandboxStatus").mockRejectedValue(failure); await expect(client.checkSandboxStatus()).rejects.toBe(failure); expect(sandboxSpy).toHaveBeenCalled(); @@ -484,7 +454,7 @@ describe("FrontalCodeApiClient", () => { mockHttpClient.get.mockRejectedValue(new Error("sandbox unavailable")); await expect(client.checkSandboxStatus()).rejects.toThrow( - "Frontal Code API error: sandbox unavailable" + "Frontal Code API error: sandbox unavailable", ); }); @@ -502,20 +472,18 @@ describe("FrontalCodeApiClient", () => { .mockRejectedValueOnce(failure); await expect(client.getTask("task-123")).rejects.toBe(failure); - await expect(client.getOrphanPolicy({ source: "slack" })).rejects.toBe( - failure - ); + await expect(client.getOrphanPolicy({ source: "slack" })).rejects.toBe(failure); await expect(client.listTasks({ source: "slack" })).rejects.toBe(failure); await expect( client.createTask({ prompt: "Investigate flaky test", - }) + }), ).rejects.toBe(failure); await expect( client.updateTaskContext({ taskId: "task-123", channel_id: "C123", - }) + }), ).rejects.toBe(failure); await expect( client.sendConnectorInteraction("slack", { @@ -526,21 +494,21 @@ describe("FrontalCodeApiClient", () => { channel: { id: "C123" }, message: { ts: "1710000000.100" }, }, - }) + }), ).rejects.toBe(failure); await expect( client.sendConnectorEvent("slack", { type: "reaction_added", userId: "U123", data: { reaction: "eyes" }, - }) + }), ).rejects.toBe(failure); await expect( client.resolveTaskApproval({ taskId: "task-123", approvalKind: "orphaned_hosted_agent", action: "retry", - }) + }), ).rejects.toBe(failure); }); @@ -557,7 +525,7 @@ describe("FrontalCodeApiClient", () => { prompt: "Investigate flaky test", source: "slack", channel_id: "C123", - }) + }), ).rejects.toBe(failure); await expect(client.listTasks({ source: "slack" })).rejects.toBe(failure); await expect( @@ -565,14 +533,14 @@ describe("FrontalCodeApiClient", () => { taskId: "task-123", source: "slack", channel_id: "C123", - }) + }), ).rejects.toBe(failure); await expect( client.sendConnectorEvent("slack", { type: "reaction_added", userId: "U123", data: { reaction: "eyes" }, - }) + }), ).rejects.toBe(failure); }); @@ -592,7 +560,7 @@ describe("FrontalCodeApiClient", () => { }); expect(url).toBe( - "wss://frontal-code.example.com/v1/events/ws?source=slack&channel_id=C123&limit=10" + "wss://frontal-code.example.com/v1/events/ws?source=slack&channel_id=C123&limit=10", ); }); }); diff --git a/extensions/frontal-code-slack/tests/bot/slack-bot-service.test.ts b/extensions/frontal-code-slack/tests/bot/slack-bot-service.test.ts index a481968..ea38c2e 100644 --- a/extensions/frontal-code-slack/tests/bot/slack-bot-service.test.ts +++ b/extensions/frontal-code-slack/tests/bot/slack-bot-service.test.ts @@ -76,59 +76,35 @@ describe("SlackBotService", () => { it("should register all handlers", () => { expect(mockSlackApp.command).toHaveBeenCalledWith( "/frontal-code-create", - expect.any(Function) + expect.any(Function), ); expect(mockSlackApp.command).toHaveBeenCalledWith( "/frontal-code-status", - expect.any(Function) - ); - expect(mockSlackApp.command).toHaveBeenCalledWith( - "/frontal-code-list", - expect.any(Function) + expect.any(Function), ); + expect(mockSlackApp.command).toHaveBeenCalledWith("/frontal-code-list", expect.any(Function)); expect(mockSlackApp.command).toHaveBeenCalledWith( "/frontal-code-pause", - expect.any(Function) + expect.any(Function), ); expect(mockSlackApp.command).toHaveBeenCalledWith( "/frontal-code-resume", - expect.any(Function) + expect.any(Function), ); expect(mockSlackApp.command).toHaveBeenCalledWith( "/frontal-code-cancel", - expect.any(Function) - ); - expect(mockSlackApp.command).toHaveBeenCalledWith( - "/frontal-code-help", - expect.any(Function) + expect.any(Function), ); + expect(mockSlackApp.command).toHaveBeenCalledWith("/frontal-code-help", expect.any(Function)); - expect(mockSlackApp.message).toHaveBeenCalledWith( - "hello frontal-code", - expect.any(Function) - ); - expect(mockSlackApp.message).toHaveBeenCalledWith( - /frontal-code (.+)/, - expect.any(Function) - ); + expect(mockSlackApp.message).toHaveBeenCalledWith("hello frontal-code", expect.any(Function)); + expect(mockSlackApp.message).toHaveBeenCalledWith(/frontal-code (.+)/, expect.any(Function)); - expect(mockSlackApp.action).toHaveBeenCalledWith( - "task_pause", - expect.any(Function) - ); - expect(mockSlackApp.action).toHaveBeenCalledWith( - "task_resume", - expect.any(Function) - ); - expect(mockSlackApp.action).toHaveBeenCalledWith( - "task_cancel", - expect.any(Function) - ); + expect(mockSlackApp.action).toHaveBeenCalledWith("task_pause", expect.any(Function)); + expect(mockSlackApp.action).toHaveBeenCalledWith("task_resume", expect.any(Function)); + expect(mockSlackApp.action).toHaveBeenCalledWith("task_cancel", expect.any(Function)); - expect(mockSlackApp.event).toHaveBeenCalledWith( - "app_mention", - expect.any(Function) - ); + expect(mockSlackApp.event).toHaveBeenCalledWith("app_mention", expect.any(Function)); }); }); @@ -155,7 +131,7 @@ describe("SlackBotService", () => { // Get the handler function and call it const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-create" + ([cmd]) => cmd === "/frontal-code-create", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -180,7 +156,7 @@ describe("SlackBotService", () => { const mockRespond = vi.fn(); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-create" + ([cmd]) => cmd === "/frontal-code-create", )?.[1]; await handler({ command: invalidCommand, @@ -201,7 +177,7 @@ describe("SlackBotService", () => { const mockRespond = vi.fn(); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-create" + ([cmd]) => cmd === "/frontal-code-create", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -222,7 +198,7 @@ describe("SlackBotService", () => { mockTaskManager.createTask.mockRejectedValue(error); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-create" + ([cmd]) => cmd === "/frontal-code-create", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -256,7 +232,7 @@ describe("SlackBotService", () => { mockTaskManager.createTask.mockResolvedValue(mockTask); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-create" + ([cmd]) => cmd === "/frontal-code-create", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -268,7 +244,7 @@ describe("SlackBotService", () => { branch: "feature/profile", model: "claude-3-sonnet", provider: "anthropic", - }) + }), ); }); }); @@ -296,7 +272,7 @@ describe("SlackBotService", () => { }); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-status" + ([cmd]) => cmd === "/frontal-code-status", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -335,7 +311,7 @@ describe("SlackBotService", () => { }); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-status" + ([cmd]) => cmd === "/frontal-code-status", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -360,7 +336,7 @@ describe("SlackBotService", () => { mockTaskManager.getTask.mockResolvedValue(null); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-status" + ([cmd]) => cmd === "/frontal-code-status", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -379,7 +355,7 @@ describe("SlackBotService", () => { mockTaskManager.getUserTasks.mockResolvedValue([]); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-status" + ([cmd]) => cmd === "/frontal-code-status", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -416,7 +392,7 @@ describe("SlackBotService", () => { mockTaskManager.getUserTasks.mockResolvedValue(mockTasks); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-list" + ([cmd]) => cmd === "/frontal-code-list", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -441,7 +417,7 @@ describe("SlackBotService", () => { mockTaskManager.getUserTasks.mockResolvedValue([]); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-list" + ([cmd]) => cmd === "/frontal-code-list", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -462,15 +438,12 @@ describe("SlackBotService", () => { mockTaskManager.cancelTask.mockResolvedValue(undefined); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-cancel" + ([cmd]) => cmd === "/frontal-code-cancel", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); expect(mockAck).toHaveBeenCalled(); - expect(mockTaskManager.cancelTask).toHaveBeenCalledWith( - "task-123", - command.user_id - ); + expect(mockTaskManager.cancelTask).toHaveBeenCalledWith("task-123", command.user_id); expect(mockRespond).toHaveBeenCalledWith({ text: "Task `task-123` has been cancelled.", response_type: "in_channel", @@ -483,7 +456,7 @@ describe("SlackBotService", () => { const mockRespond = vi.fn(); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-cancel" + ([cmd]) => cmd === "/frontal-code-cancel", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -503,7 +476,7 @@ describe("SlackBotService", () => { mockTaskManager.cancelTask.mockRejectedValue(error); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-cancel" + ([cmd]) => cmd === "/frontal-code-cancel", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -522,7 +495,7 @@ describe("SlackBotService", () => { const mockRespond = vi.fn(); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-help" + ([cmd]) => cmd === "/frontal-code-help", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -552,7 +525,7 @@ describe("SlackBotService", () => { const mockRespond = vi.fn(); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-pause" + ([cmd]) => cmd === "/frontal-code-pause", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -569,7 +542,7 @@ describe("SlackBotService", () => { const mockRespond = vi.fn(); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-resume" + ([cmd]) => cmd === "/frontal-code-resume", )?.[1]; await handler({ command, ack: mockAck, respond: mockRespond }); @@ -593,12 +566,12 @@ describe("SlackBotService", () => { const mockSay = vi.fn(); const handler = mockSlackApp.message.mock.calls.find( - ([pattern]) => pattern === "hello frontal-code" + ([pattern]) => pattern === "hello frontal-code", )?.[1]; await handler({ message, say: mockSay }); expect(mockSay).toHaveBeenCalledWith( - "Hello! I'm Frontal Code, your autonomous coding assistant. Use `/frontal-code-help` to see what I can do!" + "Hello! I'm Frontal Code, your autonomous coding assistant. Use `/frontal-code-help` to see what I can do!", ); }); }); @@ -625,13 +598,12 @@ describe("SlackBotService", () => { const handler = mockSlackApp.message.mock.calls.find( ([pattern]) => - pattern instanceof RegExp && - pattern.test("frontal-code fix the login bug") + pattern instanceof RegExp && pattern.test("frontal-code fix the login bug"), )?.[1]; await handler({ message, say: mockSay }); expect(mockSay).toHaveBeenCalledWith( - "Task created: `task-789`. I'll start working on it right away!" + "Task created: `task-789`. I'll start working on it right away!", ); }); @@ -644,13 +616,12 @@ describe("SlackBotService", () => { const mockSay = vi.fn(); const handler = mockSlackApp.message.mock.calls.find( - ([pattern]) => - pattern instanceof RegExp && pattern.test("frontal-code") + ([pattern]) => pattern instanceof RegExp && pattern.test("frontal-code"), )?.[1]; await handler({ message, say: mockSay }); expect(mockSay).toHaveBeenCalledWith( - 'Please provide a task description after "frontal-code".' + 'Please provide a task description after "frontal-code".', ); }); @@ -666,13 +637,12 @@ describe("SlackBotService", () => { mockTaskManager.createTask.mockRejectedValue(error); const handler = mockSlackApp.message.mock.calls.find( - ([pattern]) => - pattern instanceof RegExp && pattern.test("frontal-code test task") + ([pattern]) => pattern instanceof RegExp && pattern.test("frontal-code test task"), )?.[1]; await handler({ message, say: mockSay }); expect(mockSay).toHaveBeenCalledWith( - "Sorry, I couldn't create that task: Failed to create task" + "Sorry, I couldn't create that task: Failed to create task", ); }); }); @@ -696,7 +666,7 @@ describe("SlackBotService", () => { mockTaskManager.cancelTask.mockResolvedValue(undefined); const handler = mockSlackApp.action.mock.calls.find( - ([action]) => action === "task_cancel" + ([action]) => action === "task_cancel", )?.[1]; await handler({ body: interaction, @@ -705,10 +675,7 @@ describe("SlackBotService", () => { }); expect(mockAck).toHaveBeenCalled(); - expect(mockTaskManager.cancelTask).toHaveBeenCalledWith( - "task-123", - interaction.user.id - ); + expect(mockTaskManager.cancelTask).toHaveBeenCalledWith("task-123", interaction.user.id); expect(mockRespond).toHaveBeenCalledWith({ text: "Task `task-123` has been cancelled.", replace_original: true, @@ -724,7 +691,7 @@ describe("SlackBotService", () => { mockTaskManager.cancelTask.mockRejectedValue(error); const handler = mockSlackApp.action.mock.calls.find( - ([action]) => action === "task_cancel" + ([action]) => action === "task_cancel", )?.[1]; await handler({ body: interaction, @@ -747,7 +714,7 @@ describe("SlackBotService", () => { const mockRespond = vi.fn(); const handler = mockSlackApp.action.mock.calls.find( - ([action]) => action === "task_pause" + ([action]) => action === "task_pause", )?.[1]; await handler({ body: interaction, @@ -768,7 +735,7 @@ describe("SlackBotService", () => { const mockRespond = vi.fn(); const handler = mockSlackApp.action.mock.calls.find( - ([action]) => action === "task_resume" + ([action]) => action === "task_resume", )?.[1]; await handler({ body: interaction, @@ -807,12 +774,12 @@ describe("SlackBotService", () => { mockTaskManager.createTask.mockResolvedValue(mockTask); const handler = mockSlackApp.event.mock.calls.find( - ([event_type]) => event_type === "app_mention" + ([event_type]) => event_type === "app_mention", )?.[1]; await handler({ event, say: mockSay }); expect(mockSay).toHaveBeenCalledWith( - "Task created: `task-999`. I'll start working on it right away!" + "Task created: `task-999`. I'll start working on it right away!", ); }); @@ -825,13 +792,11 @@ describe("SlackBotService", () => { const mockSay = vi.fn(); const handler = mockSlackApp.event.mock.calls.find( - ([event_type]) => event_type === "app_mention" + ([event_type]) => event_type === "app_mention", )?.[1]; await handler({ event, say: mockSay }); - expect(mockSay).toHaveBeenCalledWith( - "Hi! How can I help you with your coding tasks?" - ); + expect(mockSay).toHaveBeenCalledWith("Hi! How can I help you with your coding tasks?"); }); it("should handle app mention errors", async () => { @@ -846,12 +811,12 @@ describe("SlackBotService", () => { mockTaskManager.createTask.mockRejectedValue(error); const handler = mockSlackApp.event.mock.calls.find( - ([event_type]) => event_type === "app_mention" + ([event_type]) => event_type === "app_mention", )?.[1]; await handler({ event, say: mockSay }); expect(mockSay).toHaveBeenCalledWith( - "Sorry, I couldn't create that task: Failed to create task" + "Sorry, I couldn't create that task: Failed to create task", ); }); }); @@ -873,9 +838,7 @@ describe("SlackBotService", () => { const error = new Error("Failed to start bot"); mockRedisService.connect.mockRejectedValue(error); - await expect(slackBotService.start()).rejects.toThrow( - "Failed to start bot" - ); + await expect(slackBotService.start()).rejects.toThrow("Failed to start bot"); }); }); @@ -896,9 +859,7 @@ describe("SlackBotService", () => { const error = new Error("Failed to stop bot"); mockSlackApp.stop.mockRejectedValue(error); - await expect(slackBotService.stop()).rejects.toThrow( - "Failed to stop bot" - ); + await expect(slackBotService.stop()).rejects.toThrow("Failed to stop bot"); }); }); @@ -935,9 +896,7 @@ describe("SlackBotService", () => { }); it("should handle service failures in health check", async () => { - mockSlackApp.client.auth.test.mockRejectedValue( - new Error("Slack API error") - ); + mockSlackApp.client.auth.test.mockRejectedValue(new Error("Slack API error")); mockFrontalCodeApiClient.healthCheck.mockResolvedValue(false); mockDatabaseService.healthCheck.mockResolvedValue(false); mockRedisService.healthCheck.mockResolvedValue(false); diff --git a/extensions/frontal-code-slack/tests/env-runtime.test.ts b/extensions/frontal-code-slack/tests/env-runtime.test.ts index 96aa80e..d92a7f1 100644 --- a/extensions/frontal-code-slack/tests/env-runtime.test.ts +++ b/extensions/frontal-code-slack/tests/env-runtime.test.ts @@ -4,8 +4,8 @@ const REQUIRED_ENV = { SLACK_BOT_TOKEN: "xoxb-test-token", SLACK_APP_TOKEN: "xapp-test-token", SLACK_SIGNING_SECRET: "test-signing-secret-with-at-least-32-chars", - FCODE_API_URL: "http://localhost:8787", - FCODE_API_TIMEOUT: "30000", + FRONTAL_CODE_API_URL: "http://localhost:8787", + FRONTAL_CODE_API_TIMEOUT: "30000", NODE_ENV: "test", LOG_LEVEL: "error", PORT: "3000", @@ -14,9 +14,7 @@ const REQUIRED_ENV = { HEALTH_CHECK_INTERVAL: "30000", } as const; -async function loadEnvModule( - extraEnv: Record = {} -) { +function loadEnvModule(extraEnv: Record = {}) { vi.resetModules(); for (const [key, value] of Object.entries({ ...REQUIRED_ENV, ...extraEnv })) { if (value === undefined) { @@ -53,12 +51,10 @@ describe("Environment runtime branches", () => { it("logs validation success for the compatibility helper", async () => { const module = await loadEnvModule(); - const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); module.validateEnvConfig(); - expect(logSpy).toHaveBeenCalledWith( - "Environment variables validated successfully" - ); + expect(logSpy).toHaveBeenCalledWith("Environment variables validated successfully"); }); }); diff --git a/extensions/frontal-code-slack/tests/env.test.ts b/extensions/frontal-code-slack/tests/env.test.ts index a3c0dd3..5c7b6a8 100644 --- a/extensions/frontal-code-slack/tests/env.test.ts +++ b/extensions/frontal-code-slack/tests/env.test.ts @@ -5,8 +5,8 @@ const BASE_ENV = { SLACK_BOT_TOKEN: "xoxb-test-token", SLACK_APP_TOKEN: "xapp-test-token", SLACK_SIGNING_SECRET: "test-signing-secret-with-at-least-32-chars", - FCODE_API_URL: "http://localhost:8787", - FCODE_API_TIMEOUT: "30000", + FRONTAL_CODE_API_URL: "http://localhost:8787", + FRONTAL_CODE_API_TIMEOUT: "30000", NODE_ENV: "test", LOG_LEVEL: "error", PORT: "3000", @@ -18,9 +18,7 @@ const BASE_ENV = { SKIP_ENV_VALIDATION: "", } as const; -async function importFreshEnvModule( - overrides: Record = {} -) { +function importFreshEnvModule(overrides: Record = {}) { vi.resetModules(); for (const key of Object.keys(BASE_ENV)) { @@ -59,20 +57,18 @@ describe("Environment Variables", () => { }); it("should have Frontal Code API configuration", () => { - expect(env.FCODE_API_URL).toBeDefined(); - expect(env.FCODE_API_URL).toMatch(/^https?:\/\//); - expect(env.FCODE_API_TIMEOUT).toBeDefined(); - expect(env.FCODE_API_TIMEOUT).toBeGreaterThanOrEqual(1000); - expect(env.FCODE_API_TIMEOUT).toBeLessThanOrEqual(300000); + expect(env.FRONTAL_CODE_API_URL).toBeDefined(); + expect(env.FRONTAL_CODE_API_URL).toMatch(/^https?:\/\//); + expect(env.FRONTAL_CODE_API_TIMEOUT).toBeDefined(); + expect(env.FRONTAL_CODE_API_TIMEOUT).toBeGreaterThanOrEqual(1000); + expect(env.FRONTAL_CODE_API_TIMEOUT).toBeLessThanOrEqual(300000); }); it("should have application configuration", () => { expect(env.NODE_ENV).toBeDefined(); expect(["development", "production", "test"]).toContain(env.NODE_ENV); expect(env.LOG_LEVEL).toBeDefined(); - expect(["error", "warn", "info", "http", "debug"]).toContain( - env.LOG_LEVEL - ); + expect(["error", "warn", "info", "http", "debug"]).toContain(env.LOG_LEVEL); expect(env.PORT).toBeDefined(); expect(env.PORT).toBeGreaterThanOrEqual(1000); expect(env.PORT).toBeLessThanOrEqual(65535); @@ -84,7 +80,7 @@ describe("Environment Variables", () => { const config = getEnvConfig(); expect(config).toHaveProperty("slack"); - expect(config).toHaveProperty("frontal-code"); + expect(config).toHaveProperty("frontalCode"); expect(config).toHaveProperty("app"); expect(config).toHaveProperty("limits"); @@ -112,8 +108,8 @@ describe("Environment Variables", () => { expect(config.slack.botToken).toBe(env.SLACK_BOT_TOKEN); expect(config.slack.appToken).toBe(env.SLACK_APP_TOKEN); expect(config.slack.signingSecret).toBe(env.SLACK_SIGNING_SECRET); - expect(config.frontalCode.apiUrl).toBe(env.FCODE_API_URL); - expect(config.frontalCode.timeout).toBe(env.FCODE_API_TIMEOUT); + expect(config.frontalCode.apiUrl).toBe(env.FRONTAL_CODE_API_URL); + expect(config.frontalCode.timeout).toBe(env.FRONTAL_CODE_API_TIMEOUT); expect(config.app.nodeEnv).toBe(env.NODE_ENV); expect(config.app.logLevel).toBe(env.LOG_LEVEL); expect(config.app.port).toBe(env.PORT); @@ -145,18 +141,17 @@ describe("Environment Variables", () => { describe("runtime coercion and validation", () => { it("coerces numeric runtime env strings into numbers on fresh module load", async () => { - const { env: freshEnv, getEnvConfig: loadConfig } = - await importFreshEnvModule({ - FCODE_API_TIMEOUT: "45000", - PORT: "4567", - MAX_CONCURRENT_TASKS: "7", - TASK_TIMEOUT: "120000", - HEALTH_CHECK_INTERVAL: "15000", - }); + const { env: freshEnv, getEnvConfig: loadConfig } = await importFreshEnvModule({ + FRONTAL_CODE_API_TIMEOUT: "45000", + PORT: "4567", + MAX_CONCURRENT_TASKS: "7", + TASK_TIMEOUT: "120000", + HEALTH_CHECK_INTERVAL: "15000", + }); const config = loadConfig(); - expect(freshEnv.FCODE_API_TIMEOUT).toBe(45000); + expect(freshEnv.FRONTAL_CODE_API_TIMEOUT).toBe(45000); expect(freshEnv.PORT).toBe(4567); expect(freshEnv.MAX_CONCURRENT_TASKS).toBe(7); expect(freshEnv.TASK_TIMEOUT).toBe(120000); @@ -167,8 +162,8 @@ describe("Environment Variables", () => { it("treats empty strings as undefined and falls back to schema defaults", async () => { const { env: freshEnv } = await importFreshEnvModule({ - FCODE_API_URL: "", - FCODE_API_TIMEOUT: "", + FRONTAL_CODE_API_URL: "", + FRONTAL_CODE_API_TIMEOUT: "", LOG_LEVEL: "", PORT: "", MAX_CONCURRENT_TASKS: "", @@ -176,8 +171,8 @@ describe("Environment Variables", () => { HEALTH_CHECK_INTERVAL: "", }); - expect(freshEnv.FCODE_API_URL).toBe("http://frontal-code-api:8787"); - expect(freshEnv.FCODE_API_TIMEOUT).toBe(30000); + expect(freshEnv.FRONTAL_CODE_API_URL).toBe("http://frontal-code-api:8787"); + expect(freshEnv.FRONTAL_CODE_API_TIMEOUT).toBe(30000); expect(freshEnv.LOG_LEVEL).toBe("info"); expect(freshEnv.PORT).toBe(3000); expect(freshEnv.MAX_CONCURRENT_TASKS).toBe(10); @@ -186,17 +181,15 @@ describe("Environment Variables", () => { }); it("throws a validation error for invalid runtime env values", async () => { - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); await expect( importFreshEnvModule({ SLACK_BOT_TOKEN: "invalid-token", - }) + }), ).rejects.toThrow("Environment variable validation failed"); - expect(errorSpy).toHaveBeenCalledWith( - "Environment variable validation failed:" - ); + expect(errorSpy).toHaveBeenCalledWith("Environment variable validation failed:"); }); it("throws on client-side access to server env variables", async () => { @@ -210,17 +203,14 @@ describe("Environment Variables", () => { const { env: clientEnv } = await importFreshEnvModule(); expect(() => clientEnv.SLACK_BOT_TOKEN).toThrow( - "Attempted to access server-side environment variable 'SLACK_BOT_TOKEN' on the client" + "Attempted to access server-side environment variable 'SLACK_BOT_TOKEN' on the client", ); } finally { - if (originalWindow === undefined) { - delete (globalThis as { window?: unknown }).window; - } else { - Object.defineProperty(globalThis, "window", { - value: originalWindow, - configurable: true, - }); - } + Object.defineProperty(globalThis, "window", { + value: originalWindow, + configurable: true, + writable: true, + }); } }); }); diff --git a/extensions/frontal-code-slack/tests/events-client.test.ts b/extensions/frontal-code-slack/tests/events-client.test.ts index bdfd3dd..fceb218 100644 --- a/extensions/frontal-code-slack/tests/events-client.test.ts +++ b/extensions/frontal-code-slack/tests/events-client.test.ts @@ -1,17 +1,14 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const { MockWebSocket, wsInstances } = vi.hoisted(() => { - const wsInstances: Array> = []; + const wsInstances: InstanceType[] = []; class MockWebSocket { static OPEN = 1; public readonly url: string; public readyState = 0; - private readonly handlers = new Map< - string, - Array<(...args: unknown[]) => void> - >(); + private readonly handlers = new Map void>>(); constructor(url: string) { this.url = url; @@ -50,7 +47,7 @@ vi.mock("../src/config", () => ({ logLevel: "error", port: 3000, }, - frontal_code: { + frontalCode: { apiUrl: "https://frontal-code.example.com/", timeout: 30_000, }, @@ -73,12 +70,9 @@ describe("Legacy FrontalCodeEventsClient", () => { }); it("builds a websocket URL from the hosted API URL", () => { - const client = - new FrontalCodeEventsClient() as TestableLegacyFrontalCodeEventsClient; + const client = new FrontalCodeEventsClient() as TestableLegacyFrontalCodeEventsClient; - expect(client.buildWsUrl()).toBe( - "wss://frontal-code.example.com/v1/events/ws" - ); + expect(client.buildWsUrl()).toBe("wss://frontal-code.example.com/v1/events/ws"); }); it("builds a ws URL from a non-secure hosted API URL", async () => { @@ -90,21 +84,19 @@ describe("Legacy FrontalCodeEventsClient", () => { logLevel: "error", port: 3000, }, - frontal_code: { + frontalCode: { apiUrl: "http://frontal-code.example.com/", timeout: 30_000, }, }, })); - const { FrontalCodeEventsClient: HttpFrontalCodeEventsClient } = - await import("../src/events-client"); - const client = - new HttpFrontalCodeEventsClient() as TestableLegacyFrontalCodeEventsClient; - - expect(client.buildWsUrl()).toBe( - "ws://frontal-code.example.com/v1/events/ws" + const { FrontalCodeEventsClient: HttpFrontalCodeEventsClient } = await import( + "../src/events-client" ); + const client = new HttpFrontalCodeEventsClient() as TestableLegacyFrontalCodeEventsClient; + + expect(client.buildWsUrl()).toBe("ws://frontal-code.example.com/v1/events/ws"); vi.doMock("../src/config", () => ({ config: { @@ -113,7 +105,7 @@ describe("Legacy FrontalCodeEventsClient", () => { logLevel: "error", port: 3000, }, - frontal_code: { + frontalCode: { apiUrl: "https://frontal-code.example.com/", timeout: 30_000, }, @@ -122,17 +114,14 @@ describe("Legacy FrontalCodeEventsClient", () => { }); it("builds a ws URL from an http API base by mutating the shared config object", () => { - const client = - new FrontalCodeEventsClient() as TestableLegacyFrontalCodeEventsClient; + const client = new FrontalCodeEventsClient() as TestableLegacyFrontalCodeEventsClient; const originalUrl = config.frontalCode.apiUrl; - config.frontal-code.apiUrl = "http://frontal-code.example.com/"; + config.frontalCode.apiUrl = "http://frontal-code.example.com/"; try { - expect(client.buildWsUrl()).toBe( - "ws://frontal-code.example.com/v1/events/ws" - ); + expect(client.buildWsUrl()).toBe("ws://frontal-code.example.com/v1/events/ws"); } finally { - config.frontal-code.apiUrl = originalUrl; + config.frontalCode.apiUrl = originalUrl; } }); @@ -145,7 +134,7 @@ describe("Legacy FrontalCodeEventsClient", () => { logLevel: "error", port: 3000, }, - frontal_code: { + frontalCode: { apiUrl: "ws://frontal-code.example.com/", timeout: 30_000, }, @@ -155,12 +144,9 @@ describe("Legacy FrontalCodeEventsClient", () => { const { FrontalCodeEventsClient: WsFrontalCodeEventsClient } = await import( "../src/events-client" ); - const client = - new WsFrontalCodeEventsClient() as TestableLegacyFrontalCodeEventsClient; + const client = new WsFrontalCodeEventsClient() as TestableLegacyFrontalCodeEventsClient; - expect(client.buildWsUrl()).toBe( - "ws://frontal-code.example.com/v1/events/ws" - ); + expect(client.buildWsUrl()).toBe("ws://frontal-code.example.com/v1/events/ws"); vi.doMock("../src/config", () => ({ config: { @@ -169,7 +155,7 @@ describe("Legacy FrontalCodeEventsClient", () => { logLevel: "error", port: 3000, }, - frontal_code: { + frontalCode: { apiUrl: "https://frontal-code.example.com/", timeout: 30_000, }, @@ -180,7 +166,7 @@ describe("Legacy FrontalCodeEventsClient", () => { it("dispatches parsed event payloads to registered handlers", () => { const client = new FrontalCodeEventsClient(); const handler = vi.fn(); - const infoSpy = vi.spyOn(logger, "info").mockImplementation(() => {}); + const infoSpy = vi.spyOn(logger, "info").mockImplementation(() => undefined); client.onEvent(handler); client.connect(); @@ -196,26 +182,23 @@ describe("Legacy FrontalCodeEventsClient", () => { status: "running", emittedAt: "2026-04-09T10:00:00Z", task_id: "task-123", - }) - ) + }), + ), ); expect(handler).toHaveBeenCalledWith( expect.objectContaining({ event: "lane.started", task_id: "task-123", - }) - ); - expect(infoSpy).toHaveBeenCalledWith( - "Connected to Frontal Code event stream", - { - url: "wss://frontal-code.example.com/v1/events/ws", - } + }), ); + expect(infoSpy).toHaveBeenCalledWith("Connected to Frontal Code event stream", { + url: "wss://frontal-code.example.com/v1/events/ws", + }); }); it("logs malformed event payloads without dispatching handlers", () => { - const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => {}); + const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => undefined); const client = new FrontalCodeEventsClient(); const handler = vi.fn(); client.onEvent(handler); @@ -225,15 +208,11 @@ describe("Legacy FrontalCodeEventsClient", () => { socket.emit("message", Buffer.from("not-json")); expect(handler).not.toHaveBeenCalled(); - expect(errorSpy).toHaveBeenCalledWith( - "Failed to parse Frontal Code event", - expect.any(Error) - ); + expect(errorSpy).toHaveBeenCalledWith("Failed to parse Frontal Code event", expect.any(Error)); }); it("does not reconnect when an open socket already exists", () => { - const client = - new FrontalCodeEventsClient() as TestableLegacyFrontalCodeEventsClient; + const client = new FrontalCodeEventsClient() as TestableLegacyFrontalCodeEventsClient; client.ws = { readyState: MockWebSocket.OPEN, close: vi.fn(), @@ -273,8 +252,7 @@ describe("Legacy FrontalCodeEventsClient", () => { it("cancels a pending reconnect when the client is closed", () => { vi.useFakeTimers(); - const client = - new FrontalCodeEventsClient() as TestableLegacyFrontalCodeEventsClient; + const client = new FrontalCodeEventsClient() as TestableLegacyFrontalCodeEventsClient; client.connect(); const firstSocket = wsInstances[0]; diff --git a/extensions/frontal-code-slack/tests/frontal-code-events.test.ts b/extensions/frontal-code-slack/tests/frontal-code-events.test.ts index 451d413..34e1447 100644 --- a/extensions/frontal-code-slack/tests/frontal-code-events.test.ts +++ b/extensions/frontal-code-slack/tests/frontal-code-events.test.ts @@ -1,21 +1,15 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const { MockWebSocket, wsInstances } = vi.hoisted(() => { - const wsInstances: Array> = []; + const wsInstances: InstanceType[] = []; class MockWebSocket { public static readonly OPEN = 1; public static readonly CLOSED = 3; public readonly url: string; public readyState = 0; - private readonly handlers = new Map< - string, - Array<(...args: unknown[]) => void> - >(); - private readonly onceHandlers = new Map< - string, - Array<(...args: unknown[]) => void> - >(); + private readonly handlers = new Map void>>(); + private readonly onceHandlers = new Map void>>(); constructor(url: string) { this.url = url; @@ -62,21 +56,13 @@ vi.mock("ws", () => ({ import { FrontalCodeEventsClient } from "../src/frontal-code-events"; import { logger } from "../src/log"; -import type { - FrontalCodeEventEnvelope, - FrontalCodeTrackedTask, -} from "../src/types"; +import type { FrontalCodeEventEnvelope, FrontalCodeTrackedTask } from "../src/types"; type TestableFrontalCodeEventsClient = FrontalCodeEventsClient & { buildSocketUrl(): string; handleMessage(payload: string): void; - dispatchEvent( - event: FrontalCodeEventEnvelope, - hintedTask?: FrontalCodeTrackedTask - ): void; - readTrackedTaskFromEvent( - event: FrontalCodeEventEnvelope - ): FrontalCodeTrackedTask | undefined; + dispatchEvent(event: FrontalCodeEventEnvelope, hintedTask?: FrontalCodeTrackedTask): void; + readTrackedTaskFromEvent(event: FrontalCodeEventEnvelope): FrontalCodeTrackedTask | undefined; scheduleReconnect(): void; recentEvents: Map; reconnectTimer?: NodeJS.Timeout; @@ -86,7 +72,7 @@ type TestableFrontalCodeEventsClient = FrontalCodeEventsClient & { }; function createEventEnvelope( - overrides: Partial = {} + overrides: Partial = {}, ): FrontalCodeEventEnvelope { return { event_id: "evt-123", @@ -113,12 +99,8 @@ describe("FrontalCodeEventsClient", () => { beforeEach(() => { wsInstances.length = 0; vi.useRealTimers(); - urlBuilder = vi - .fn() - .mockReturnValue("ws://localhost:8787/v1/events/ws?source=slack"); - client = new FrontalCodeEventsClient( - urlBuilder - ) as unknown as TestableFrontalCodeEventsClient; + urlBuilder = vi.fn().mockReturnValue("ws://localhost:8787/v1/events/ws?source=slack"); + client = new FrontalCodeEventsClient(urlBuilder) as unknown as TestableFrontalCodeEventsClient; }); it("builds a slack-scoped websocket subscription URL", () => { @@ -163,7 +145,7 @@ describe("FrontalCodeEventsClient", () => { expect.objectContaining({ taskId: "task-123", channelId: "C123", - }) + }), ); }); @@ -174,8 +156,8 @@ describe("FrontalCodeEventsClient", () => { createEventEnvelope({ event_id: `evt-${index}`, emittedAt: `2026-04-09T10:00:${String(index).padStart(2, "0")}Z`, - }) - ) + }), + ), ); } @@ -185,7 +167,7 @@ describe("FrontalCodeEventsClient", () => { expect(bufferedEvents?.[0]).toEqual( expect.objectContaining({ event_id: "evt-2", - }) + }), ); }); @@ -203,7 +185,7 @@ describe("FrontalCodeEventsClient", () => { channelId: "C123", threadTs: "1710000000.100", userId: "U123", - }) + }), ); }); @@ -220,8 +202,8 @@ describe("FrontalCodeEventsClient", () => { payload: { worker_status: "running", }, - }) - ) + }), + ), ); expect(handler).toHaveBeenLastCalledWith( @@ -231,7 +213,7 @@ describe("FrontalCodeEventsClient", () => { expect.objectContaining({ taskId: "task-123", channelId: "C123", - }) + }), ); }); @@ -253,7 +235,7 @@ describe("FrontalCodeEventsClient", () => { expect.objectContaining({ taskId: "task-123", channelId: "C123", - }) + }), ); }); @@ -302,14 +284,14 @@ describe("FrontalCodeEventsClient", () => { createEventEnvelope({ emittedAt: "2026-04-09T10:00:00Z", lane_id: "lane-1", - }) + }), ); client.dispatchEvent( createEventEnvelope({ event_id: "evt-124", emittedAt: "2026-04-09T10:00:00Z", lane_id: "lane-2", - }) + }), ); expect(handler).toHaveBeenCalledTimes(2); @@ -324,7 +306,7 @@ describe("FrontalCodeEventsClient", () => { emittedAt: "2026-04-09T10:00:00Z", lane_id: "lane-123", status: "running", - }) + }), ); client.dispatchEvent( createEventEnvelope({ @@ -332,7 +314,7 @@ describe("FrontalCodeEventsClient", () => { emittedAt: "2026-04-09T10:00:00Z", lane_id: "lane-123", status: "failed", - }) + }), ); expect(handler).toHaveBeenCalledTimes(2); @@ -348,7 +330,7 @@ describe("FrontalCodeEventsClient", () => { status: "running", emittedAt: "2026-04-09T10:00:00Z", lane_id: "lane-123", - }) + }), ); client.dispatchEvent( createEventEnvelope({ @@ -357,7 +339,7 @@ describe("FrontalCodeEventsClient", () => { status: "running", emittedAt: "2026-04-09T10:00:00Z", lane_id: "lane-123", - }) + }), ); expect(handler).toHaveBeenCalledTimes(2); @@ -388,14 +370,14 @@ describe("FrontalCodeEventsClient", () => { event_id: "evt-456", emittedAt: "2026-04-09T10:00:01Z", }), - trackedTask + trackedTask, ); expect(handler).toHaveBeenCalledWith(expect.any(Object), trackedTask); }); it("logs handler failures without stopping dispatch", () => { - const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => {}); + const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => undefined); const failingHandler = vi.fn().mockRejectedValue(new Error("boom")); const nextHandler = vi.fn(); client.onTrackedTaskEvent(failingHandler); @@ -413,25 +395,22 @@ describe("FrontalCodeEventsClient", () => { expect.objectContaining({ event: "lane.started", taskId: "task-123", - }) + }), ); }); }); it("ignores malformed hosted event payloads", () => { - const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); + const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => undefined); const handler = vi.fn(); client.onTrackedTaskEvent(handler); client.handleMessage("not-json"); expect(handler).not.toHaveBeenCalled(); - expect(warnSpy).toHaveBeenCalledWith( - "Ignoring malformed Frontal Code hosted event payload", - { - payload: "not-json", - } - ); + expect(warnSpy).toHaveBeenCalledWith("Ignoring malformed Frontal Code hosted event payload", { + payload: "not-json", + }); }); it("ignores events without a task id", () => { @@ -442,8 +421,8 @@ describe("FrontalCodeEventsClient", () => { JSON.stringify( createEventEnvelope({ task_id: undefined, - }) - ) + }), + ), ); expect(handler).not.toHaveBeenCalled(); @@ -469,7 +448,7 @@ describe("FrontalCodeEventsClient", () => { payload: { channel_id: "C999", }, - }) + }), ); expect(task).toEqual({ @@ -485,8 +464,8 @@ describe("FrontalCodeEventsClient", () => { client.readTrackedTaskFromEvent( createEventEnvelope({ payload: undefined, - }) - ) + }), + ), ).toBeUndefined(); expect( @@ -496,8 +475,8 @@ describe("FrontalCodeEventsClient", () => { payload: { channel_id: "C123", }, - }) - ) + }), + ), ).toBeUndefined(); }); @@ -520,7 +499,7 @@ describe("FrontalCodeEventsClient", () => { client.dispatchEvent( createEventEnvelope({ task_id: undefined, - }) + }), ); expect(handler).not.toHaveBeenCalled(); @@ -552,7 +531,7 @@ describe("FrontalCodeEventsClient", () => { event_id: `evt-${index}`, emittedAt: `2026-04-09T10:${String(Math.floor(index / 60)).padStart(2, "0")}:${String(index % 60).padStart(2, "0")}Z`, lane_id: `lane-${index}`, - }) + }), ); } @@ -563,7 +542,7 @@ describe("FrontalCodeEventsClient", () => { event_id: "evt-0-replay", emittedAt: "2026-04-09T10:00:00Z", lane_id: "lane-0", - }) + }), ); expect(handler).toHaveBeenCalledTimes(514); @@ -625,7 +604,7 @@ describe("FrontalCodeEventsClient", () => { it("reschedules reconnect when a reconnect attempt fails to open", async () => { vi.useFakeTimers(); - const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => {}); + const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => undefined); const connectPromise = client.connect(); const firstSocket = wsInstances[0]; firstSocket.emit("open"); @@ -642,9 +621,8 @@ describe("FrontalCodeEventsClient", () => { expect( errorSpy.mock.calls.some( ([message, error]) => - message === "Frontal Code hosted events reconnect failed" && - error instanceof Error - ) + message === "Frontal Code hosted events reconnect failed" && error instanceof Error, + ), ).toBe(true); vi.advanceTimersByTime(2_000); @@ -666,7 +644,7 @@ describe("FrontalCodeEventsClient", () => { it("logs restart failures and schedules a reconnect when immediate restart open fails", async () => { vi.useFakeTimers(); - const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => {}); + const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => undefined); const connectPromise = client.connect(); const firstSocket = wsInstances[0]; firstSocket.emit("open"); @@ -683,9 +661,8 @@ describe("FrontalCodeEventsClient", () => { expect( errorSpy.mock.calls.some( ([message, error]) => - message === "Frontal Code hosted events stream restart failed" && - error instanceof Error - ) + message === "Frontal Code hosted events stream restart failed" && error instanceof Error, + ), ).toBe(true); vi.advanceTimersByTime(2_000); diff --git a/extensions/frontal-code-slack/tests/integration/slack-workflow.test.ts b/extensions/frontal-code-slack/tests/integration/slack-workflow.test.ts index eeab573..4600d90 100644 --- a/extensions/frontal-code-slack/tests/integration/slack-workflow.test.ts +++ b/extensions/frontal-code-slack/tests/integration/slack-workflow.test.ts @@ -54,7 +54,7 @@ describe("Slack Workflow Integration Tests", () => { // Get the create command handler const { mockSlackApp } = await import("../setup"); const createHandler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-create" + ([cmd]) => cmd === "/frontal-code-create", )?.[1]; // Execute the command @@ -108,7 +108,7 @@ describe("Slack Workflow Integration Tests", () => { // Get the status command handler const statusHandler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-status" + ([cmd]) => cmd === "/frontal-code-status", )?.[1]; // Execute the status command @@ -155,7 +155,7 @@ describe("Slack Workflow Integration Tests", () => { // Get the cancel command handler const cancelHandler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-cancel" + ([cmd]) => cmd === "/frontal-code-cancel", )?.[1]; // Execute the cancel command @@ -195,7 +195,7 @@ describe("Slack Workflow Integration Tests", () => { // Get the create command handler const { mockSlackApp } = await import("../setup"); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-create" + ([cmd]) => cmd === "/frontal-code-create", )?.[1]; // Execute the invalid command @@ -233,14 +233,12 @@ describe("Slack Workflow Integration Tests", () => { // Mock service error const { mockTaskManager } = await import("../setup"); - mockTaskManager.createTask.mockRejectedValue( - new Error("Service unavailable") - ); + mockTaskManager.createTask.mockRejectedValue(new Error("Service unavailable")); // Get the create command handler const { mockSlackApp } = await import("../setup"); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-create" + ([cmd]) => cmd === "/frontal-code-create", )?.[1]; // Execute the command @@ -306,7 +304,7 @@ describe("Slack Workflow Integration Tests", () => { // Get the create command handler const { mockSlackApp } = await import("../setup"); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-create" + ([cmd]) => cmd === "/frontal-code-create", )?.[1]; // Execute both commands concurrently @@ -330,14 +328,8 @@ describe("Slack Workflow Integration Tests", () => { expect(mockRespond2).toHaveBeenCalled(); // Verify different tasks were created - expect(mockTaskManager.createTask).toHaveBeenCalledWith( - "U123456", - expect.any(Object) - ); - expect(mockTaskManager.createTask).toHaveBeenCalledWith( - "U789012", - expect.any(Object) - ); + expect(mockTaskManager.createTask).toHaveBeenCalledWith("U123456", expect.any(Object)); + expect(mockTaskManager.createTask).toHaveBeenCalledWith("U789012", expect.any(Object)); }); }); @@ -367,7 +359,7 @@ describe("Slack Workflow Integration Tests", () => { // Get the app mention handler const { mockSlackApp } = await import("../setup"); const handler = mockSlackApp.event.mock.calls.find( - ([event_type]) => event_type === "app_mention" + ([event_type]) => event_type === "app_mention", )?.[1]; // Execute the mention handler @@ -375,7 +367,7 @@ describe("Slack Workflow Integration Tests", () => { // Verify task creation response expect(mockSay).toHaveBeenCalledWith( - "Task created: `task-mention-123`. I'll start working on it right away!" + "Task created: `task-mention-123`. I'll start working on it right away!", ); }); @@ -415,7 +407,7 @@ describe("Slack Workflow Integration Tests", () => { // Get the task cancel action handler const { mockSlackApp } = await import("../setup"); const handler = mockSlackApp.action.mock.calls.find( - ([action]) => action === "task_cancel" + ([action]) => action === "task_cancel", )?.[1]; // Execute the interaction handler @@ -423,10 +415,7 @@ describe("Slack Workflow Integration Tests", () => { // Verify cancellation response expect(mockAck).toHaveBeenCalled(); - expect(mockTaskManager.cancelTask).toHaveBeenCalledWith( - "task-cancel-123", - "U123456" - ); + expect(mockTaskManager.cancelTask).toHaveBeenCalledWith("task-cancel-123", "U123456"); expect(mockRespond).toHaveBeenCalledWith({ text: "Task `task-cancel-123` has been cancelled.", replace_original: true, @@ -469,7 +458,7 @@ describe("Slack Workflow Integration Tests", () => { // Get the create command handler const { mockSlackApp } = await import("../setup"); const handler = mockSlackApp.command.mock.calls.find( - ([cmd]) => cmd === "/frontal-code-create" + ([cmd]) => cmd === "/frontal-code-create", )?.[1]; // Execute the command @@ -480,7 +469,7 @@ describe("Slack Workflow Integration Tests", () => { "U123456", expect.objectContaining({ prompt: "Create a new user profile page", - }) + }), ); // Verify response was sent to user @@ -562,9 +551,7 @@ describe("Slack Workflow Integration Tests", () => { mockTaskManager, } = await import("../setup"); - mockSlackApp.client.auth.test.mockRejectedValue( - new Error("Slack API error") - ); + mockSlackApp.client.auth.test.mockRejectedValue(new Error("Slack API error")); mockFrontalCodeApiClient.healthCheck.mockResolvedValue(false); mockDatabaseService.healthCheck.mockResolvedValue(true); mockRedisService.healthCheck.mockResolvedValue(false); diff --git a/extensions/frontal-code-slack/tests/log.test.ts b/extensions/frontal-code-slack/tests/log.test.ts index c8a23fc..22d2646 100644 --- a/extensions/frontal-code-slack/tests/log.test.ts +++ b/extensions/frontal-code-slack/tests/log.test.ts @@ -94,10 +94,7 @@ describe("log helpers", () => { expect(innerLogger.warn).toHaveBeenCalledWith("warn message", { scope: "warn", }); - expect(sentry.captureMessage).toHaveBeenCalledWith( - "warn message", - "warning" - ); + expect(sentry.captureMessage).toHaveBeenCalledWith("warn message", "warning"); }); it("captures exceptions and plain error messages in production error logs", async () => { @@ -114,7 +111,7 @@ describe("log helpers", () => { taskId: "task-123", error: "boom", stack: expect.any(String), - }) + }), ); expect(innerLogger.error).toHaveBeenNthCalledWith( 2, @@ -122,13 +119,10 @@ describe("log helpers", () => { expect.objectContaining({ error: undefined, stack: undefined, - }) + }), ); expect(sentry.captureException).toHaveBeenCalledWith(error); - expect(sentry.captureMessage).toHaveBeenCalledWith( - "error without exception", - "error" - ); + expect(sentry.captureMessage).toHaveBeenCalledWith("error without exception", "error"); }); it("logs task, API, Slack, user, system, security, database, and redis helper events", async () => { @@ -150,14 +144,14 @@ describe("log helpers", () => { prompt: "x".repeat(100), category: "task", action: "created", - }) + }), ); expect(module.innerLogger.info).toHaveBeenCalledWith( "Task status changed", expect.objectContaining({ oldStatus: "pending", newStatus: "running", - }) + }), ); expect(module.innerLogger.info).toHaveBeenCalledWith( "API call", @@ -166,7 +160,7 @@ describe("log helpers", () => { method: "POST", duration: 25, success: true, - }) + }), ); expect(module.innerLogger.info).toHaveBeenCalledWith( "Slack event", @@ -174,7 +168,7 @@ describe("log helpers", () => { eventType: "message", userId: "U123", channelId: "C123", - }) + }), ); expect(module.innerLogger.info).toHaveBeenCalledWith( "User action", @@ -184,14 +178,14 @@ describe("log helpers", () => { taskId: "task-123", category: "user", event_type: "action", - }) + }), ); expect(module.innerLogger.info).toHaveBeenCalledWith( "System event", expect.objectContaining({ event: "booted", region: "eu-west", - }) + }), ); expect(module.innerLogger.warn).toHaveBeenCalledWith( "Security event", @@ -199,7 +193,7 @@ describe("log helpers", () => { event: "permission_denied", userId: "U123", channelId: "C123", - }) + }), ); expect(module.innerLogger.info).toHaveBeenCalledWith( "Database operation", @@ -208,7 +202,7 @@ describe("log helpers", () => { table: "tasks", duration: 10, rows: 1, - }) + }), ); expect(module.innerLogger.info).toHaveBeenCalledWith( "Redis operation", @@ -217,7 +211,7 @@ describe("log helpers", () => { key: "task:123", duration: 5, cache: "tasks", - }) + }), ); }); @@ -234,7 +228,7 @@ describe("log helpers", () => { status: "healthy", latency: 12, category: "health", - }) + }), ); expect(module.innerLogger.error).toHaveBeenCalledWith( "Health check - frontal-code", @@ -243,7 +237,7 @@ describe("log helpers", () => { status: "unhealthy", reason: "timeout", category: "health", - }) + }), ); }); @@ -274,7 +268,7 @@ describe("log helpers", () => { operation: "sync", duration: 42, tasks: 3, - }) + }), ); expect(module.innerLogger.info).toHaveBeenLastCalledWith( "Performance metric", @@ -283,7 +277,7 @@ describe("log helpers", () => { duration: 125, source: "slack", result: "ok", - }) + }), ); vi.useRealTimers(); diff --git a/extensions/frontal-code-slack/tests/setup-current.ts b/extensions/frontal-code-slack/tests/setup-current.ts index 9263d6e..30f003c 100644 --- a/extensions/frontal-code-slack/tests/setup-current.ts +++ b/extensions/frontal-code-slack/tests/setup-current.ts @@ -5,8 +5,8 @@ process.env.LOG_LEVEL = "error"; process.env.SLACK_BOT_TOKEN = "xoxb-test-token"; process.env.SLACK_APP_TOKEN = "xapp-test-token"; process.env.SLACK_SIGNING_SECRET = "test-signing-secret-with-at-least-32-chars"; -process.env.FCODE_API_URL = "http://localhost:8787"; -process.env.FCODE_API_TIMEOUT = "30000"; +process.env.FRONTAL_CODE_API_URL = "http://localhost:8787"; +process.env.FRONTAL_CODE_API_TIMEOUT = "30000"; process.env.PORT = "3000"; process.env.MAX_CONCURRENT_TASKS = "10"; process.env.TASK_TIMEOUT = "3600000"; @@ -16,10 +16,10 @@ process.env.SENTRY_DSN = ""; process.env.SKIP_ENV_VALIDATION = ""; beforeAll(() => { - vi.spyOn(console, "log").mockImplementation(() => {}); - vi.spyOn(console, "info").mockImplementation(() => {}); - vi.spyOn(console, "warn").mockImplementation(() => {}); - vi.spyOn(console, "error").mockImplementation(() => {}); + vi.spyOn(console, "log").mockImplementation(() => undefined); + vi.spyOn(console, "info").mockImplementation(() => undefined); + vi.spyOn(console, "warn").mockImplementation(() => undefined); + vi.spyOn(console, "error").mockImplementation(() => undefined); }); afterAll(() => { diff --git a/extensions/frontal-code-slack/tests/setup-simple.ts b/extensions/frontal-code-slack/tests/setup-simple.ts index 25e0749..20ea84a 100644 --- a/extensions/frontal-code-slack/tests/setup-simple.ts +++ b/extensions/frontal-code-slack/tests/setup-simple.ts @@ -6,17 +6,17 @@ process.env.LOG_LEVEL = "error"; process.env.SLACK_BOT_TOKEN = "xoxb-test-token"; process.env.SLACK_APP_TOKEN = "xapp-test-token"; process.env.SLACK_SIGNING_SECRET = "test-signing-secret-with-at-least-32-chars"; -process.env.FCODE_API_URL = "http://localhost:8787"; +process.env.FRONTAL_CODE_API_URL = "http://localhost:8787"; process.env.DATABASE_URL = "postgresql://test:test@localhost:5432/test_db"; process.env.REDIS_URL = "redis://localhost:6379"; // Global test setup beforeAll(() => { // Mock console methods to reduce noise in tests - vi.spyOn(console, "log").mockImplementation(() => {}); - vi.spyOn(console, "info").mockImplementation(() => {}); - vi.spyOn(console, "warn").mockImplementation(() => {}); - vi.spyOn(console, "error").mockImplementation(() => {}); + vi.spyOn(console, "log").mockImplementation(() => undefined); + vi.spyOn(console, "info").mockImplementation(() => undefined); + vi.spyOn(console, "warn").mockImplementation(() => undefined); + vi.spyOn(console, "error").mockImplementation(() => undefined); }); afterAll(() => { @@ -58,11 +58,11 @@ export const createMockTaskCreationRequest = (overrides = {}) => ({ }); export const createMockSlackTask = (overrides = {}) => ({ - id: '123', - slack_task_id: 'task-123', - frontal-code_task_id: 'frontal-code-123', - user_id: 'U123456', - status: 'pending', + id: "123", + slack_task_id: "task-123", + frontal_code_task_id: "frontal-code-123", + user_id: "U123456", + status: "pending", request: createMockTaskCreationRequest(), created_at: new Date(), updated_at: new Date(), diff --git a/extensions/frontal-code-slack/tests/setup.ts b/extensions/frontal-code-slack/tests/setup.ts index 236e118..8886200 100644 --- a/extensions/frontal-code-slack/tests/setup.ts +++ b/extensions/frontal-code-slack/tests/setup.ts @@ -8,7 +8,7 @@ process.env.LOG_LEVEL = "error"; process.env.SLACK_BOT_TOKEN = "xoxb-test-token"; process.env.SLACK_APP_TOKEN = "xapp-test-token"; process.env.SLACK_SIGNING_SECRET = "test-signing-secret-with-at-least-32-chars"; -process.env.FCODE_API_URL = "http://localhost:8787"; +process.env.FRONTAL_CODE_API_URL = "http://localhost:8787"; process.env.DATABASE_URL = "postgresql://test:test@localhost:5432/test_db"; process.env.REDIS_URL = "redis://localhost:6379"; @@ -77,7 +77,7 @@ export const server = setupServer( team: "test-team", bot_id: "B123456", }); - }) + }), ); // Global test setup @@ -86,10 +86,10 @@ beforeAll(() => { server.listen(); // Mock console methods to reduce noise in tests - vi.spyOn(console, "log").mockImplementation(() => {}); - vi.spyOn(console, "info").mockImplementation(() => {}); - vi.spyOn(console, "warn").mockImplementation(() => {}); - vi.spyOn(console, "error").mockImplementation(() => {}); + vi.spyOn(console, "log").mockImplementation(() => undefined); + vi.spyOn(console, "info").mockImplementation(() => undefined); + vi.spyOn(console, "warn").mockImplementation(() => undefined); + vi.spyOn(console, "error").mockImplementation(() => undefined); }); afterAll(() => { @@ -160,11 +160,11 @@ export const createMockTaskCreationRequest = (overrides = {}) => ({ }); export const createMockSlackTask = (overrides = {}) => ({ - id: '123', - slack_task_id: 'task-123', - frontal-code_task_id: 'frontal-code-123', - user_id: 'U123456', - status: 'pending', + id: "123", + slack_task_id: "task-123", + frontal_code_task_id: "frontal-code-123", + user_id: "U123456", + status: "pending", request: createMockTaskCreationRequest(), created_at: new Date(), updated_at: new Date(), diff --git a/extensions/frontal-code-slack/tests/slack-behavior.test.ts b/extensions/frontal-code-slack/tests/slack-behavior.test.ts index 49bd6a2..3dd9561 100644 --- a/extensions/frontal-code-slack/tests/slack-behavior.test.ts +++ b/extensions/frontal-code-slack/tests/slack-behavior.test.ts @@ -17,27 +17,24 @@ type TestableSlackInterface = SlackInterface & { registerTask( task: FrontalCodeCreateTaskResponse, trackedTask: FrontalCodeTrackedTask, - prompt: string - ): Promise; - handleSlackAction( - action: { action_id: string; value?: string }, - body: SlackBody + prompt: string, ): Promise; + handleSlackAction(action: { action_id: string; value?: string }, body: SlackBody): Promise; handleOrphanApprovalAction( action: { action_id: string; value?: string }, - body: SlackBody + body: SlackBody, ): Promise; handleFrontalCodeTaskEvent( event: FrontalCodeEventEnvelope, - task?: FrontalCodeTrackedTask + task?: FrontalCodeTrackedTask, ): Promise; resolveTrackedTaskForEvent( event: FrontalCodeEventEnvelope, - task?: FrontalCodeTrackedTask + task?: FrontalCodeTrackedTask, ): Promise; hydrateTrackedTaskFromEvent( task: FrontalCodeTrackedTask, - event: FrontalCodeEventEnvelope + event: FrontalCodeEventEnvelope, ): FrontalCodeTrackedTask; upsertTrackedTask(task: FrontalCodeTrackedTask): void; syncTrackedTasksFromFrontalCode(): Promise; @@ -48,9 +45,7 @@ type TestableSlackInterface = SlackInterface & { }; function createTestSlackInterface() { - const slack = Object.create( - SlackInterface.prototype - ) as TestableSlackInterface; + const slack = Object.create(SlackInterface.prototype) as TestableSlackInterface; const postMessage = vi.fn(); const updateMessage = vi.fn(); const updateTaskContext = vi.fn(); @@ -79,7 +74,7 @@ function createTestSlackInterface() { }, }, }, - frontal-codeApi: { + frontalCodeApi: { updateTaskContext, getTask, listTasks, @@ -87,7 +82,7 @@ function createTestSlackInterface() { healthCheck, sendConnectorInteraction, }, - frontal-codeEvents: { + frontalCodeEvents: { connect: connectEvents, disconnect: disconnectEvents, trackTask, @@ -120,7 +115,7 @@ function createTestSlackInterface() { } function createTrackedTask( - overrides: Partial = {} + overrides: Partial = {}, ): FrontalCodeTrackedTask { return { taskId: "task-123", @@ -132,7 +127,7 @@ function createTrackedTask( } function createTaskResponse( - overrides: Partial = {} + overrides: Partial = {}, ): FrontalCodeCreateTaskResponse { return { task_id: "task-123", @@ -142,9 +137,7 @@ function createTaskResponse( }; } -function createTaskSnapshot( - overrides: Partial = {} -): FrontalCodeTask { +function createTaskSnapshot(overrides: Partial = {}): FrontalCodeTask { return { task_id: "task-123", prompt: "Investigate flaky test", @@ -155,9 +148,7 @@ function createTaskSnapshot( }; } -function createEvent( - overrides: Partial = {} -): FrontalCodeEventEnvelope { +function createEvent(overrides: Partial = {}): FrontalCodeEventEnvelope { return { event_id: "evt-123", topic: "approval", @@ -183,8 +174,7 @@ describe("SlackInterface behavior", () => { }); it("registers a created task, persists thread context, and tracks it for Frontal Code events", async () => { - const { slack, postMessage, updateTaskContext, trackTask } = - createTestSlackInterface(); + const { slack, postMessage, updateTaskContext, trackTask } = createTestSlackInterface(); const task = createTaskResponse({ plan_kind: "implementer", worker_status: "ready", @@ -223,9 +213,8 @@ describe("SlackInterface behavior", () => { }); it("connects by syncing tasks, starting Slack, and connecting Frontal Code events", async () => { - const infoSpy = vi.spyOn(logger, "info").mockImplementation(() => {}); - const { slack, listTasks, appStart, connectEvents } = - createTestSlackInterface(); + const infoSpy = vi.spyOn(logger, "info").mockImplementation(() => undefined); + const { slack, listTasks, appStart, connectEvents } = createTestSlackInterface(); listTasks.mockResolvedValue([]); appStart.mockResolvedValue(undefined); connectEvents.mockResolvedValue(undefined); @@ -242,22 +231,18 @@ describe("SlackInterface behavior", () => { }); it("rethrows connect failures after logging them", async () => { - const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => {}); - const { slack, listTasks, appStart, connectEvents } = - createTestSlackInterface(); + const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => undefined); + const { slack, listTasks, appStart, connectEvents } = createTestSlackInterface(); listTasks.mockResolvedValue([]); appStart.mockResolvedValue(undefined); connectEvents.mockRejectedValue(new Error("ws down")); await expect(slack.connect()).rejects.toThrow("ws down"); - expect(errorSpy).toHaveBeenCalledWith( - "Failed to connect to Slack", - expect.any(Error) - ); + expect(errorSpy).toHaveBeenCalledWith("Failed to connect to Slack", expect.any(Error)); }); it("disconnects Frontal Code events before stopping Slack", async () => { - const infoSpy = vi.spyOn(logger, "info").mockImplementation(() => {}); + const infoSpy = vi.spyOn(logger, "info").mockImplementation(() => undefined); const { slack, disconnectEvents, appStop } = createTestSlackInterface(); disconnectEvents.mockResolvedValue(undefined); appStop.mockResolvedValue(undefined); @@ -266,22 +251,17 @@ describe("SlackInterface behavior", () => { expect(disconnectEvents).toHaveBeenCalled(); expect(appStop).toHaveBeenCalled(); - expect(infoSpy).toHaveBeenCalledWith( - "Slack WebSocket interface disconnected" - ); + expect(infoSpy).toHaveBeenCalledWith("Slack WebSocket interface disconnected"); }); it("rethrows disconnect failures after logging them", async () => { - const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => {}); + const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => undefined); const { slack, disconnectEvents, appStop } = createTestSlackInterface(); disconnectEvents.mockRejectedValue(new Error("disconnect failed")); await expect(slack.disconnect()).rejects.toThrow("disconnect failed"); expect(appStop).not.toHaveBeenCalled(); - expect(errorSpy).toHaveBeenCalledWith( - "Failed to disconnect from Slack", - expect.any(Error) - ); + expect(errorSpy).toHaveBeenCalledWith("Failed to disconnect from Slack", expect.any(Error)); }); it("reports Slack and Frontal Code health from the live clients", async () => { @@ -297,7 +277,7 @@ describe("SlackInterface behavior", () => { it("treats Slack as disconnected when the Bolt client does not expose isListening", async () => { const { slack, healthCheck } = createTestSlackInterface(); - delete (slack.app as { isListening?: () => boolean }).isListening; + (slack.app as { isListening?: () => boolean }).isListening = undefined; healthCheck.mockResolvedValue(true); await expect(slack.healthCheck()).resolves.toEqual({ @@ -307,7 +287,7 @@ describe("SlackInterface behavior", () => { }); it("returns a degraded health response when health checks throw", async () => { - const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => {}); + const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => undefined); const { slack, isListening } = createTestSlackInterface(); isListening.mockImplementation(() => { throw new Error("listener unavailable"); @@ -317,23 +297,19 @@ describe("SlackInterface behavior", () => { slack: false, frontal_code: false, }); - expect(errorSpy).toHaveBeenCalledWith( - "Health check failed", - expect.any(Error) - ); + expect(errorSpy).toHaveBeenCalledWith("Health check failed", expect.any(Error)); }); it("still tracks a task when thread-anchor persistence back to Frontal Code fails", async () => { - const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); - const { slack, postMessage, updateTaskContext, trackTask } = - createTestSlackInterface(); + const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => undefined); + const { slack, postMessage, updateTaskContext, trackTask } = createTestSlackInterface(); postMessage.mockResolvedValue({ ts: "1710000000.250" }); updateTaskContext.mockRejectedValue(new Error("frontal-code unavailable")); await slack.registerTask( createTaskResponse(), createTrackedTask({ threadTs: undefined }), - "Investigate flaky test" + "Investigate flaky test", ); expect(trackTask).toHaveBeenCalledWith({ @@ -348,13 +324,12 @@ describe("SlackInterface behavior", () => { expect.objectContaining({ taskId: "task-123", error: "frontal-code unavailable", - }) + }), ); }); it("routes generic connector actions through the hosted connector interaction endpoint", async () => { - const { slack, sendConnectorInteraction, updateMessage } = - createTestSlackInterface(); + const { slack, sendConnectorInteraction, updateMessage } = createTestSlackInterface(); const body: SlackBody = { user: { id: "U123" }, channel: { id: "C123" }, @@ -374,7 +349,7 @@ describe("SlackInterface behavior", () => { action_id: "connector.action", value: "task-123", }, - body + body, ); expect(sendConnectorInteraction).toHaveBeenCalledWith("slack", { @@ -392,9 +367,7 @@ describe("SlackInterface behavior", () => { it("routes orphan approval actions through the dedicated approval handler", async () => { const { slack, sendConnectorInteraction } = createTestSlackInterface(); - const approvalSpy = vi - .spyOn(slack, "handleOrphanApprovalAction") - .mockResolvedValue(undefined); + const approvalSpy = vi.spyOn(slack, "handleOrphanApprovalAction").mockResolvedValue(undefined); const body: SlackBody = { user: { id: "U123" }, channel: { id: "C123" }, @@ -406,7 +379,7 @@ describe("SlackInterface behavior", () => { action_id: "orphaned_hosted_agent.retry", value: "task-123", }, - body + body, ); expect(approvalSpy).toHaveBeenCalledWith( @@ -414,7 +387,7 @@ describe("SlackInterface behavior", () => { action_id: "orphaned_hosted_agent.retry", value: "task-123", }, - body + body, ); expect(sendConnectorInteraction).not.toHaveBeenCalled(); }); @@ -433,7 +406,7 @@ describe("SlackInterface behavior", () => { action_id: "orphaned_hosted_agent.retry", value: "task-123", }, - body + body, ); expect(updateMessage).toHaveBeenCalledWith({ @@ -459,7 +432,7 @@ describe("SlackInterface behavior", () => { action_id: "orphaned_hosted_agent.cancel", value: "task-123", }, - body + body, ); expect(updateMessage).toHaveBeenCalledWith({ @@ -471,8 +444,7 @@ describe("SlackInterface behavior", () => { }); it("ignores orphan approval actions that do not carry a task id", async () => { - const { slack, updateMessage, resolveTaskApproval } = - createTestSlackInterface(); + const { slack, updateMessage, resolveTaskApproval } = createTestSlackInterface(); const body: SlackBody = { user: { id: "U123" }, channel: { id: "C123" }, @@ -483,7 +455,7 @@ describe("SlackInterface behavior", () => { { action_id: "orphaned_hosted_agent.retry", }, - body + body, ); expect(updateMessage).not.toHaveBeenCalled(); @@ -491,8 +463,7 @@ describe("SlackInterface behavior", () => { }); it("resolves orphan approvals through Frontal Code and updates the approval message twice", async () => { - const { slack, updateMessage, resolveTaskApproval } = - createTestSlackInterface(); + const { slack, updateMessage, resolveTaskApproval } = createTestSlackInterface(); const body: SlackBody = { user: { id: "U123" }, channel: { id: "C123" }, @@ -503,7 +474,7 @@ describe("SlackInterface behavior", () => { status: "running", worker_status: "ready_for_prompt", worker_id: "worker-123", - }) + }), ); await slack.handleOrphanApprovalAction( @@ -511,7 +482,7 @@ describe("SlackInterface behavior", () => { action_id: "orphaned_hosted_agent.retry", value: "task-123", }, - body + body, ); expect(resolveTaskApproval).toHaveBeenCalledWith({ @@ -527,7 +498,7 @@ describe("SlackInterface behavior", () => { channel: "C123", ts: "1710000000.540", text: "Processing approval for task task-123: retry.", - }) + }), ); expect(updateMessage).toHaveBeenNthCalledWith( 2, @@ -535,15 +506,14 @@ describe("SlackInterface behavior", () => { channel: "C123", ts: "1710000000.540", text: "Approval resolved for task task-123: retry.", - }) + }), ); expect(slack.approvalInFlight.has("task-123")).toBe(false); expect(slack.approvalResolved.has("task-123")).toBe(true); }); it("updates the approval message with an error state when resolution fails", async () => { - const { slack, updateMessage, resolveTaskApproval } = - createTestSlackInterface(); + const { slack, updateMessage, resolveTaskApproval } = createTestSlackInterface(); const body: SlackBody = { user: { id: "U123" }, channel: { id: "C123" }, @@ -557,28 +527,27 @@ describe("SlackInterface behavior", () => { action_id: "orphaned_hosted_agent.cancel", value: "task-123", }, - body - ) + body, + ), ).rejects.toThrow("provider down"); expect(updateMessage).toHaveBeenNthCalledWith( 1, expect.objectContaining({ text: "Processing approval for task task-123: cancel.", - }) + }), ); expect(updateMessage).toHaveBeenNthCalledWith( 2, expect.objectContaining({ text: "Approval failed for task task-123.", - }) + }), ); expect(slack.approvalInFlight.has("task-123")).toBe(false); }); it("resolves orphan approvals, updates the message twice, and marks the approval resolved", async () => { - const { slack, updateMessage, resolveTaskApproval } = - createTestSlackInterface(); + const { slack, updateMessage, resolveTaskApproval } = createTestSlackInterface(); const body: SlackBody = { user: { id: "U123" }, channel: { id: "C123" }, @@ -588,7 +557,7 @@ describe("SlackInterface behavior", () => { createTaskSnapshot({ worker_status: "ready_for_prompt", worker_id: "worker-123", - }) + }), ); await slack.handleOrphanApprovalAction( @@ -596,7 +565,7 @@ describe("SlackInterface behavior", () => { action_id: "orphaned_hosted_agent.retry", value: "task-123", }, - body + body, ); expect(resolveTaskApproval).toHaveBeenCalledWith({ @@ -613,7 +582,7 @@ describe("SlackInterface behavior", () => { ts: "1710000000.530", text: "Processing approval for task task-123: retry.", blocks: expect.any(Array), - }) + }), ); expect(updateMessage).toHaveBeenNthCalledWith( 2, @@ -622,18 +591,15 @@ describe("SlackInterface behavior", () => { ts: "1710000000.530", text: "Approval resolved for task task-123: retry.", blocks: expect.any(Array), - }) + }), ); expect(slack.approvalInFlight.has("task-123")).toBe(false); expect(slack.approvalResolved.has("task-123")).toBe(true); - expect(slack.approvalMessageTsByTask.get("task-123")).toBe( - "1710000000.530" - ); + expect(slack.approvalMessageTsByTask.get("task-123")).toBe("1710000000.530"); }); it("shows an approval error state, clears in-flight state, and rethrows when resolution fails", async () => { - const { slack, updateMessage, resolveTaskApproval } = - createTestSlackInterface(); + const { slack, updateMessage, resolveTaskApproval } = createTestSlackInterface(); const body: SlackBody = { user: { id: "U123" }, channel: { id: "C123" }, @@ -648,8 +614,8 @@ describe("SlackInterface behavior", () => { action_id: "orphaned_hosted_agent.cancel", value: "task-123", }, - body - ) + body, + ), ).rejects.toBe(failure); expect(updateMessage).toHaveBeenNthCalledWith( @@ -659,7 +625,7 @@ describe("SlackInterface behavior", () => { ts: "1710000000.540", text: "Processing approval for task task-123: cancel.", blocks: expect.any(Array), - }) + }), ); expect(updateMessage).toHaveBeenNthCalledWith( 2, @@ -668,18 +634,15 @@ describe("SlackInterface behavior", () => { ts: "1710000000.540", text: "Approval failed for task task-123.", blocks: expect.any(Array), - }) + }), ); expect(slack.approvalInFlight.has("task-123")).toBe(false); expect(slack.approvalResolved.has("task-123")).toBe(false); - expect(slack.approvalMessageTsByTask.get("task-123")).toBe( - "1710000000.540" - ); + expect(slack.approvalMessageTsByTask.get("task-123")).toBe("1710000000.540"); }); it("leaves the approval marked in-flight when the initial Slack approval update fails", async () => { - const { slack, updateMessage, resolveTaskApproval } = - createTestSlackInterface(); + const { slack, updateMessage, resolveTaskApproval } = createTestSlackInterface(); const body: SlackBody = { user: { id: "U123" }, channel: { id: "C123" }, @@ -693,21 +656,18 @@ describe("SlackInterface behavior", () => { action_id: "orphaned_hosted_agent.retry", value: "task-123", }, - body - ) + body, + ), ).rejects.toThrow("slack update failed"); expect(resolveTaskApproval).not.toHaveBeenCalled(); expect(slack.approvalInFlight.has("task-123")).toBe(true); expect(slack.approvalResolved.has("task-123")).toBe(false); - expect(slack.approvalMessageTsByTask.get("task-123")).toBe( - "1710000000.550" - ); + expect(slack.approvalMessageTsByTask.get("task-123")).toBe("1710000000.550"); }); it("posts approval-request events into the task thread and persists the approval message ts", async () => { - const { slack, postMessage, updateTaskContext } = - createTestSlackInterface(); + const { slack, postMessage, updateTaskContext } = createTestSlackInterface(); const trackedTask = createTrackedTask(); postMessage.mockResolvedValue({ ts: "1710000000.300" }); updateTaskContext.mockResolvedValue({}); @@ -720,9 +680,7 @@ describe("SlackInterface behavior", () => { blocks: expect.any(Array), thread_ts: "1710000000.100", }); - expect(slack.approvalMessageTsByTask.get("task-123")).toBe( - "1710000000.300" - ); + expect(slack.approvalMessageTsByTask.get("task-123")).toBe("1710000000.300"); expect(updateTaskContext).toHaveBeenCalledWith({ taskId: "task-123", source: "slack", @@ -735,9 +693,7 @@ describe("SlackInterface behavior", () => { it("returns without posting when an Frontal Code event cannot be resolved to a tracked task", async () => { const { slack, postMessage } = createTestSlackInterface(); - const resolveSpy = vi - .spyOn(slack, "resolveTrackedTaskForEvent") - .mockResolvedValue(undefined); + const resolveSpy = vi.spyOn(slack, "resolveTrackedTaskForEvent").mockResolvedValue(undefined); await slack.handleFrontalCodeTaskEvent(createEvent()); @@ -756,9 +712,8 @@ describe("SlackInterface behavior", () => { }); it("keeps the posted approval message even when approval-message persistence back to Frontal Code fails", async () => { - const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); - const { slack, postMessage, updateTaskContext } = - createTestSlackInterface(); + const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => undefined); + const { slack, postMessage, updateTaskContext } = createTestSlackInterface(); const trackedTask = createTrackedTask(); postMessage.mockResolvedValue({ ts: "1710000000.301" }); updateTaskContext.mockRejectedValue(new Error("persist failed")); @@ -766,15 +721,13 @@ describe("SlackInterface behavior", () => { await slack.handleFrontalCodeTaskEvent(createEvent(), trackedTask); expect(postMessage).toHaveBeenCalled(); - expect(slack.approvalMessageTsByTask.get("task-123")).toBe( - "1710000000.301" - ); + expect(slack.approvalMessageTsByTask.get("task-123")).toBe("1710000000.301"); expect(warnSpy).toHaveBeenCalledWith( "Failed to persist Slack approval message linkage to Frontal Code", expect.objectContaining({ taskId: "task-123", error: "persist failed", - }) + }), ); }); @@ -796,7 +749,7 @@ describe("SlackInterface behavior", () => { repository: "frontal-code/slack", }, }), - trackedTask + trackedTask, ); expect(updateMessage).toHaveBeenCalledWith({ @@ -811,16 +764,13 @@ describe("SlackInterface behavior", () => { }); it("cleans up tracked task state after a terminal approval-resolved update succeeds", async () => { - const { slack, postMessage, updateMessage, untrackTask } = - createTestSlackInterface(); + const { slack, postMessage, updateMessage, untrackTask } = createTestSlackInterface(); const trackedTask = createTrackedTask(); slack.trackedTasks.set("task-123", trackedTask); slack.approvalMessageTsByTask.set("task-123", "1710000000.300"); slack.approvalInFlight.add("task-123"); updateMessage.mockResolvedValue({}); - vi.spyOn(slack as never, "isTerminalFrontalCodeEvent").mockReturnValue( - true - ); + vi.spyOn(slack as never, "isTerminalFrontalCodeEvent").mockReturnValue(true); await slack.handleFrontalCodeTaskEvent( createEvent({ @@ -834,7 +784,7 @@ describe("SlackInterface behavior", () => { repository: "frontal-code/slack", }, }), - trackedTask + trackedTask, ); expect(updateMessage).toHaveBeenCalledWith({ @@ -866,7 +816,7 @@ describe("SlackInterface behavior", () => { channel_id: "C123", }, }), - trackedTask + trackedTask, ); expect(updateMessage).not.toHaveBeenCalled(); @@ -887,7 +837,7 @@ describe("SlackInterface behavior", () => { event: "task.created", status: "pending", }), - createTrackedTask() + createTrackedTask(), ); expect(postMessage).not.toHaveBeenCalled(); @@ -916,7 +866,7 @@ describe("SlackInterface behavior", () => { repository: "frontal-code/slack", }, }), - trackedTask + trackedTask, ); expect(postMessage).toHaveBeenCalledWith({ @@ -948,15 +898,13 @@ describe("SlackInterface behavior", () => { repository: "frontal-code/slack", }, }), - trackedTask - ) + trackedTask, + ), ).rejects.toThrow("slack update failed"); expect(untrackTask).not.toHaveBeenCalled(); expect(slack.trackedTasks.get("task-123")).toEqual(trackedTask); - expect(slack.approvalMessageTsByTask.get("task-123")).toBe( - "1710000000.300" - ); + expect(slack.approvalMessageTsByTask.get("task-123")).toBe("1710000000.300"); expect(slack.approvalInFlight.has("task-123")).toBe(false); expect(slack.approvalResolved.has("task-123")).toBe(true); }); @@ -980,7 +928,7 @@ describe("SlackInterface behavior", () => { repository: "frontal-code/slack", }, }), - trackedTask + trackedTask, ); expect(postMessage).toHaveBeenCalledWith({ @@ -1005,14 +953,14 @@ describe("SlackInterface behavior", () => { thread_ts: "1710000000.900", user_id: "U999", approval_message_ts: "1710000001.000", - }) + }), ); const resolvedTask = await slack.resolveTrackedTaskForEvent( createEvent({ task_id: "task-unknown", payload: {}, - }) + }), ); expect(getTask).toHaveBeenCalledWith("task-unknown"); @@ -1028,9 +976,7 @@ describe("SlackInterface behavior", () => { threadTs: "1710000000.900", userId: "U999", }); - expect(slack.approvalMessageTsByTask.get("task-unknown")).toBe( - "1710000001.000" - ); + expect(slack.approvalMessageTsByTask.get("task-unknown")).toBe("1710000001.000"); }); it("returns no tracked task when an event arrives without a task id", async () => { @@ -1039,7 +985,7 @@ describe("SlackInterface behavior", () => { const resolvedTask = await slack.resolveTrackedTaskForEvent( createEvent({ task_id: undefined, - }) + }), ); expect(resolvedTask).toBeUndefined(); @@ -1054,7 +1000,7 @@ describe("SlackInterface behavior", () => { channelId: "C123", threadTs: undefined, userId: undefined, - }) + }), ); const resolvedTask = await slack.resolveTrackedTaskForEvent( @@ -1065,7 +1011,7 @@ describe("SlackInterface behavior", () => { user_id: "U999", repository: "frontal-code/slack", }, - }) + }), ); expect(resolvedTask).toEqual({ @@ -1088,7 +1034,7 @@ describe("SlackInterface behavior", () => { thread_ts: "1710000000.456", user_id: "U456", }, - }) + }), ); expect(getTask).not.toHaveBeenCalled(); @@ -1118,7 +1064,7 @@ describe("SlackInterface behavior", () => { user_id: "U777", repository: "frontal-code/slack", }, - }) + }), ); expect(getTask).not.toHaveBeenCalled(); @@ -1138,7 +1084,7 @@ describe("SlackInterface behavior", () => { }); it("returns no tracked task when fallback Frontal Code lookup fails", async () => { - const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); + const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => undefined); const { slack, getTask } = createTestSlackInterface(); getTask.mockRejectedValue(new Error("not found")); @@ -1146,7 +1092,7 @@ describe("SlackInterface behavior", () => { createEvent({ task_id: "task-missing", payload: {}, - }) + }), ); expect(resolvedTask).toBeUndefined(); @@ -1155,7 +1101,7 @@ describe("SlackInterface behavior", () => { expect.objectContaining({ taskId: "task-missing", error: "not found", - }) + }), ); }); @@ -1167,14 +1113,14 @@ describe("SlackInterface behavior", () => { channel_id: undefined, thread_ts: "1710000000.901", user_id: "U901", - }) + }), ); const resolvedTask = await slack.resolveTrackedTaskForEvent( createEvent({ task_id: "task-no-routing", payload: {}, - }) + }), ); expect(resolvedTask).toBeUndefined(); @@ -1237,19 +1183,16 @@ describe("SlackInterface behavior", () => { }); it("logs and continues when startup sync from Frontal Code fails", async () => { - const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); + const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => undefined); const { slack, listTasks, trackTask } = createTestSlackInterface(); listTasks.mockRejectedValue(new Error("frontal-code down")); await slack.syncTrackedTasksFromFrontalCode(); expect(trackTask).not.toHaveBeenCalled(); - expect(warnSpy).toHaveBeenCalledWith( - "Failed to synchronize Slack tasks from Frontal Code", - { - error: "frontal-code down", - } - ); + expect(warnSpy).toHaveBeenCalledWith("Failed to synchronize Slack tasks from Frontal Code", { + error: "frontal-code down", + }); }); it("hydrates known tracked tasks with newer routing details from later events", async () => { @@ -1260,7 +1203,7 @@ describe("SlackInterface behavior", () => { channelId: "C123", threadTs: undefined, userId: undefined, - }) + }), ); const resolvedTask = await slack.resolveTrackedTaskForEvent( @@ -1271,7 +1214,7 @@ describe("SlackInterface behavior", () => { user_id: "U456", repository: "frontal-code/slack", }, - }) + }), ); expect(getTask).not.toHaveBeenCalled(); @@ -1293,7 +1236,7 @@ describe("SlackInterface behavior", () => { channelId: "C123", threadTs: "1710000000.100", userId: "U123", - }) + }), ); const resolvedTask = await slack.resolveTrackedTaskForEvent( @@ -1303,7 +1246,7 @@ describe("SlackInterface behavior", () => { user_id: "U456", repository: "frontal-code/slack", }, - }) + }), ); expect(resolvedTask).toEqual({ @@ -1326,8 +1269,8 @@ describe("SlackInterface behavior", () => { payload: { repository: "frontal-code/slack", }, - }) - ) + }), + ), ).toBe(task); }); @@ -1340,7 +1283,7 @@ describe("SlackInterface behavior", () => { channelId: "C456", threadTs: undefined, userId: "U456", - }) + }), ); expect(slack.trackedTasks.get("task-123")).toEqual({ diff --git a/extensions/frontal-code-slack/tests/slack-constructor.test.ts b/extensions/frontal-code-slack/tests/slack-constructor.test.ts index f622e66..9f42fab 100644 --- a/extensions/frontal-code-slack/tests/slack-constructor.test.ts +++ b/extensions/frontal-code-slack/tests/slack-constructor.test.ts @@ -1,9 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const slackState = vi.hoisted(() => { - let messageHandler: - | ((args: { message: unknown }) => Promise) - | undefined; + let messageHandler: ((args: { message: unknown }) => Promise) | undefined; let commandHandler: | ((args: { command: { text: string; user_id: string; channel_id: string }; @@ -11,11 +9,7 @@ const slackState = vi.hoisted(() => { }) => Promise) | undefined; let actionHandler: - | ((args: { - action: unknown; - ack: () => Promise | void; - body: unknown; - }) => Promise) + | ((args: { action: unknown; ack: () => Promise | void; body: unknown }) => Promise) | undefined; let eventHandler: ((args: { event: unknown }) => Promise) | undefined; @@ -37,8 +31,6 @@ const slackState = vi.hoisted(() => { stop = appStop; isListening = isListening; - constructor(_options: unknown) {} - message(handler: typeof messageHandler): void { messageHandler = handler; } @@ -76,8 +68,7 @@ const slackState = vi.hoisted(() => { }; }); -const frontal; --codeApiState = vi.hoisted(() => { +const frontalCodeApiState = vi.hoisted(() => { const createTask = vi.fn(); const getOrphanPolicy = vi.fn(); const sendConnectorEvent = vi.fn(); @@ -101,11 +92,8 @@ const frontal; }; }); -const frontal; --codeEventsState = vi.hoisted(() => { - let trackedHandler: - | ((event: unknown, task: unknown) => Promise | void) - | undefined; +const frontalCodeEventsState = vi.hoisted(() => { + let trackedHandler: ((event: unknown, task: unknown) => Promise | void) | undefined; const connect = vi.fn(); const disconnect = vi.fn(); @@ -113,7 +101,7 @@ const frontal; const untrackTask = vi.fn(); const onTrackedTaskEvent = vi.fn((handler: typeof trackedHandler) => { trackedHandler = handler; - return () => {}; + return () => undefined; }); class MockFrontalCodeEventsClient { @@ -122,8 +110,6 @@ const frontal; trackTask = trackTask; untrackTask = untrackTask; onTrackedTaskEvent = onTrackedTaskEvent; - - constructor(_builder: unknown) {} } return { @@ -149,8 +135,7 @@ vi.mock("../src/api-client", () => ({ })); vi.mock("../src/frontal-code-events", () => ({ - FrontalCodeEventsClient: - frontalCodeEventsState.MockFrontalCodeEventsClient, + FrontalCodeEventsClient: frontalCodeEventsState.MockFrontalCodeEventsClient, })); vi.mock("../src/config", () => ({ @@ -165,7 +150,7 @@ vi.mock("../src/config", () => ({ appToken: "xapp-test", signingSecret: "secret", }, - frontal_code: { + frontalCode: { apiUrl: "http://localhost:8787", timeout: 30_000, }, @@ -187,30 +172,24 @@ describe("SlackInterface constructor wiring", () => { SlackInterface.prototype as unknown as { handleFrontalCodeTaskEvent: (...args: unknown[]) => Promise; }, - "handleFrontalCodeTaskEvent" + "handleFrontalCodeTaskEvent", ) .mockResolvedValue(undefined); - const slack = new SlackInterface(); + const _slack = new SlackInterface(); expect(frontalCodeApiState.getEventsWebSocketUrl).not.toHaveBeenCalled(); - expect(frontalCodeEventsState.onTrackedTaskEvent).toHaveBeenCalledTimes( - 1 - ); + expect(frontalCodeEventsState.onTrackedTaskEvent).toHaveBeenCalledTimes(1); expect(slackState.getMessageHandler()).toBeTypeOf("function"); expect(slackState.getCommandHandler()).toBeTypeOf("function"); expect(slackState.getActionHandler()).toBeTypeOf("function"); expect(slackState.getEventHandler()).toBeTypeOf("function"); - (await frontal) - - codeEventsState.getTrackedHandler()?.( - { event: "lane.started" }, - { taskId: "task-123" } - ); - - expect(handleSpy).toHaveBeenCalledWith( + await frontalCodeEventsState.getTrackedHandler()?.( { event: "lane.started" }, - { taskId: "task-123" } + { taskId: "task-123" }, ); + + expect(handleSpy).toHaveBeenCalledWith({ event: "lane.started" }, { taskId: "task-123" }); }); it("ignores Slack message events without a user-facing prompt", async () => { @@ -243,15 +222,14 @@ describe("SlackInterface constructor wiring", () => { SlackInterface.prototype as unknown as { registerTask: (...args: unknown[]) => Promise; }, - "registerTask" + "registerTask", ) .mockResolvedValue(undefined); - frontal - - codeApiState.createTask.mockResolvedValue({ - task_id: "task-123", - status: "running", - message: "created", - }); + frontalCodeApiState.createTask.mockResolvedValue({ + task_id: "task-123", + status: "running", + message: "created", + }); new SlackInterface(); await slackState.getMessageHandler()?.({ @@ -278,18 +256,17 @@ describe("SlackInterface constructor wiring", () => { threadTs: "1710000000.100", userId: "U123", }, - "Investigate flaky test" + "Investigate flaky test", ); }); it("acknowledges and returns policy previews for /ai policy orphans", async () => { const ack = vi.fn(); - frontal - - codeApiState.getOrphanPolicy.mockResolvedValue({ - default_policy: { source: "default", approval_delay_secs: 60 }, - effective_policy: { source: "default", approval_delay_secs: 60 }, - configured_rules: [], - }); + frontalCodeApiState.getOrphanPolicy.mockResolvedValue({ + default_policy: { source: "default", approval_delay_secs: 60 }, + effective_policy: { source: "default", approval_delay_secs: 60 }, + configured_rules: [], + }); new SlackInterface(); await slackState.getCommandHandler()?.({ @@ -305,17 +282,14 @@ describe("SlackInterface constructor wiring", () => { expect(ack).toHaveBeenCalledWith( expect.objectContaining({ response_type: "ephemeral", - }) + }), ); expect(frontalCodeApiState.createTask).not.toHaveBeenCalled(); }); it("acknowledges policy preview failures with an ephemeral error response", async () => { const ack = vi.fn(); - frontal - - codeApiState.getOrphanPolicy.mockRejectedValue( - new Error("policy unavailable") - ); + frontalCodeApiState.getOrphanPolicy.mockRejectedValue(new Error("policy unavailable")); new SlackInterface(); await slackState.getCommandHandler()?.({ @@ -341,15 +315,14 @@ describe("SlackInterface constructor wiring", () => { SlackInterface.prototype as unknown as { registerTask: (...args: unknown[]) => Promise; }, - "registerTask" + "registerTask", ) .mockResolvedValue(undefined); - frontal - - codeApiState.createTask.mockResolvedValue({ - task_id: "task-456", - status: "running", - message: "created", - }); + frontalCodeApiState.createTask.mockResolvedValue({ + task_id: "task-456", + status: "running", + message: "created", + }); new SlackInterface(); await slackState.getCommandHandler()?.({ @@ -375,7 +348,7 @@ describe("SlackInterface constructor wiring", () => { channelId: "C123", userId: "U123", }, - "Fix the flaky test" + "Fix the flaky test", ); }); @@ -386,7 +359,7 @@ describe("SlackInterface constructor wiring", () => { SlackInterface.prototype as unknown as { handleSlackAction: (...args: unknown[]) => Promise; }, - "handleSlackAction" + "handleSlackAction", ) .mockResolvedValue(undefined); new SlackInterface(); @@ -418,18 +391,15 @@ describe("SlackInterface constructor wiring", () => { }, }); - expect(frontalCodeApiState.sendConnectorEvent).toHaveBeenCalledWith( - "slack", - { + expect(frontalCodeApiState.sendConnectorEvent).toHaveBeenCalledWith("slack", { + type: "reaction_added", + userId: "U123", + data: { type: "reaction_added", - userId: "U123", - data: { - type: "reaction_added", - user: "U123", - reaction: "eyes", - }, - } - ); + user: "U123", + reaction: "eyes", + }, + }); }); it("forwards Slack events without a user as connector events with an empty user id", async () => { @@ -442,16 +412,13 @@ describe("SlackInterface constructor wiring", () => { }, }); - expect(frontalCodeApiState.sendConnectorEvent).toHaveBeenCalledWith( - "slack", - { + expect(frontalCodeApiState.sendConnectorEvent).toHaveBeenCalledWith("slack", { + type: "member_joined_channel", + userId: "", + data: { type: "member_joined_channel", - userId: "", - data: { - type: "member_joined_channel", - channel: "C123", - }, - } - ); + channel: "C123", + }, + }); }); }); diff --git a/extensions/frontal-code-slack/tests/slack-formatting.test.ts b/extensions/frontal-code-slack/tests/slack-formatting.test.ts index 91f24cd..e49034d 100644 --- a/extensions/frontal-code-slack/tests/slack-formatting.test.ts +++ b/extensions/frontal-code-slack/tests/slack-formatting.test.ts @@ -12,7 +12,7 @@ import type { type TestableSlackInterface = SlackInterface & { formatFrontalCodeEvent( event: FrontalCodeEventEnvelope, - task: FrontalCodeTrackedTask + task: FrontalCodeTrackedTask, ): { text: string; blocks?: Array<{ @@ -22,33 +22,27 @@ type TestableSlackInterface = SlackInterface & { }>; } | null; parseOrphanPolicyCommand( - text: string + text: string, ): { repository?: string; source?: string; priority?: string } | null; buildOrphanPolicyCommandResponse(policy: FrontalCodeOrphanPolicyResponse): { response_type: "ephemeral"; text: string; blocks: Array<{ type: string; text?: { text: string } }>; }; - describeOrphanPolicy( - policy?: FrontalCodeAppliedOrphanPolicy - ): string | undefined; - formatOrphanPolicyRuleLines( - policy: FrontalCodeOrphanPolicyResponse - ): string[]; - readEventTaskSummary( - event: FrontalCodeEventEnvelope - ): FrontalCodeEventTaskSummary; + describeOrphanPolicy(policy?: FrontalCodeAppliedOrphanPolicy): string | undefined; + formatOrphanPolicyRuleLines(policy: FrontalCodeOrphanPolicyResponse): string[]; + readEventTaskSummary(event: FrontalCodeEventEnvelope): FrontalCodeEventTaskSummary; formatLaneStartedEvent( taskLabel: string, event: FrontalCodeEventEnvelope, - summary: FrontalCodeEventTaskSummary + summary: FrontalCodeEventTaskSummary, ): { text: string; }; formatLaneFailedEvent( taskLabel: string, event: FrontalCodeEventEnvelope, - summary: FrontalCodeEventTaskSummary + summary: FrontalCodeEventTaskSummary, ): { text: string; }; @@ -59,18 +53,18 @@ type TestableSlackInterface = SlackInterface & { worker_status?: string; worker_id?: string; }, - prompt: string + prompt: string, ): string; formatTaskRoutedEvent( taskLabel: string, event: FrontalCodeEventEnvelope, - summary: FrontalCodeEventTaskSummary + summary: FrontalCodeEventTaskSummary, ): { text: string; }; formatLaneBlockedEvent( taskLabel: string, - event: FrontalCodeEventEnvelope + event: FrontalCodeEventEnvelope, ): { text: string; }; @@ -78,7 +72,7 @@ type TestableSlackInterface = SlackInterface & { task: FrontalCodeTrackedTask, taskLabel: string, event: FrontalCodeEventEnvelope, - summary: FrontalCodeEventTaskSummary + summary: FrontalCodeEventTaskSummary, ): { text: string; blocks?: Array<{ @@ -89,7 +83,7 @@ type TestableSlackInterface = SlackInterface & { }; buildApprovalProcessingBlocks( taskId: string, - action: FrontalCodeApprovalAction + action: FrontalCodeApprovalAction, ): Array<{ type: string; text?: { text: string }; @@ -98,14 +92,14 @@ type TestableSlackInterface = SlackInterface & { taskId: string, action: string, workerStatus?: string, - workerId?: string + workerId?: string, ): Array<{ type: string; text?: { text: string }; }>; buildApprovalErrorBlocks( taskId: string, - error: Error + error: Error, ): Array<{ type: string; text?: { text: string }; @@ -113,7 +107,7 @@ type TestableSlackInterface = SlackInterface & { }>; formatApprovalRequestedEvent( event: FrontalCodeEventEnvelope, - task: FrontalCodeTrackedTask + task: FrontalCodeTrackedTask, ): { text: string; blocks?: Array<{ @@ -133,7 +127,7 @@ describe("SlackInterface formatting helpers", () => { const slack = createSlackInterface(); const query = slack.parseOrphanPolicyCommand( - "policy orphans repo=frontal-code/slack source=slack priority=high" + "policy orphans repo=frontal-code/slack source=slack priority=high", ); expect(query).toEqual({ @@ -147,7 +141,7 @@ describe("SlackInterface formatting helpers", () => { const slack = createSlackInterface(); const query = slack.parseOrphanPolicyCommand( - "policy orphans repo=frontal-code/slack source= priority=high invalid unknown=value" + "policy orphans repo=frontal-code/slack source= priority=high invalid unknown=value", ); expect(query).toEqual({ @@ -168,7 +162,7 @@ describe("SlackInterface formatting helpers", () => { const slack = createSlackInterface(); const query = slack.parseOrphanPolicyCommand( - "policy orphans foo=bar repo=frontal-code/slack broken source= priority=high" + "policy orphans foo=bar repo=frontal-code/slack broken source= priority=high", ); expect(query).toEqual({ @@ -208,9 +202,7 @@ describe("SlackInterface formatting helpers", () => { expect(response.response_type).toBe("ephemeral"); expect(response.text).toContain("repo=frontal-code/slack"); - expect(response.blocks[1]?.text?.text).toContain( - "1. repo=frontal-code/slack, source=slack" - ); + expect(response.blocks[1]?.text?.text).toContain("1. repo=frontal-code/slack, source=slack"); }); it("renders orphan policy previews without selectors or scoped rules", () => { @@ -227,12 +219,8 @@ describe("SlackInterface formatting helpers", () => { configured_rules: [], }); - expect(response.text).toContain( - "Orphan policy: Policy: default; approval 60s." - ); - expect(response.blocks[1]?.text?.text).toContain( - "No scoped rules configured." - ); + expect(response.text).toContain("Orphan policy: Policy: default; approval 60s."); + expect(response.blocks[1]?.text?.text).toContain("No scoped rules configured."); }); it("renders orphan policy previews when only non-repository selectors are present", () => { @@ -256,9 +244,7 @@ describe("SlackInterface formatting helpers", () => { }); expect(response.text).toContain("preview for source=slack, priority=high"); - expect(response.blocks[0]?.text?.text).toContain( - "Selectors: source=slack, priority=high" - ); + expect(response.blocks[0]?.text?.text).toContain("Selectors: source=slack, priority=high"); }); it("renders policy unavailable fallbacks when effective and default policies are missing", () => { @@ -272,18 +258,10 @@ describe("SlackInterface formatting helpers", () => { configured_rules: [], }); - expect(response.text).toContain( - "preview for repo=frontal-code/slack: Policy unavailable." - ); - expect(response.blocks[0]?.text?.text).toContain( - "Effective: Policy unavailable." - ); - expect(response.blocks[0]?.text?.text).toContain( - "Default: Policy unavailable." - ); - expect(response.blocks[1]?.text?.text).toContain( - "No scoped rules configured." - ); + expect(response.text).toContain("preview for repo=frontal-code/slack: Policy unavailable."); + expect(response.blocks[0]?.text?.text).toContain("Effective: Policy unavailable."); + expect(response.blocks[0]?.text?.text).toContain("Default: Policy unavailable."); + expect(response.blocks[1]?.text?.text).toContain("No scoped rules configured."); }); it("formats orphan policy text with selectors and optional retry/cancel timing", () => { @@ -298,9 +276,9 @@ describe("SlackInterface formatting helpers", () => { approval_delay_secs: 30, auto_retry_after_secs: 90, auto_cancel_after_secs: 300, - }) + }), ).toBe( - "Policy: rule (repo=frontal-code/slack, source=slack, priority=high); approval 30s, auto-retry 90s, auto-cancel 300s." + "Policy: rule (repo=frontal-code/slack, source=slack, priority=high); approval 30s, auto-retry 90s, auto-cancel 300s.", ); expect(slack.describeOrphanPolicy()).toBeUndefined(); }); @@ -429,7 +407,7 @@ describe("SlackInterface formatting helpers", () => { worker_id: "worker-123", }, }, - {} + {}, ); expect(message).toEqual({ @@ -446,8 +424,8 @@ describe("SlackInterface formatting helpers", () => { task_id: "task-123", worker_status: "ready_for_prompt", }, - "Investigate flaky test" - ) + "Investigate flaky test", + ), ).toContain("Worker: ready_for_prompt"); }); @@ -467,7 +445,7 @@ describe("SlackInterface formatting helpers", () => { plan_kind: "reviewer", }, }, - {} + {}, ); expect(message).toEqual({ @@ -493,7 +471,7 @@ describe("SlackInterface formatting helpers", () => { }, { plan_kind: "implementer", - } + }, ); expect(message).toEqual({ @@ -514,7 +492,7 @@ describe("SlackInterface formatting helpers", () => { emittedAt: "2026-04-09T10:10:02Z", task_id: "task-123", }, - {} + {}, ); expect(message).toEqual({ @@ -540,7 +518,7 @@ describe("SlackInterface formatting helpers", () => { }, { error: "summary error", - } + }, ); expect(message).toEqual({ @@ -561,7 +539,7 @@ describe("SlackInterface formatting helpers", () => { emittedAt: "2026-04-09T10:10:03Z", task_id: "task-123", }, - {} + {}, ); expect(message).toEqual({ @@ -627,13 +605,11 @@ describe("SlackInterface formatting helpers", () => { worker_id: "worker-123", }, }, - {} + {}, ); expect(message.text).toBe("Approval resolved for task task-123: updated."); - expect(message.blocks?.[0]?.text?.text).toContain( - "Worker: ready_for_prompt (worker-123)" - ); + expect(message.blocks?.[0]?.text?.text).toContain("Worker: ready_for_prompt (worker-123)"); }); it("formats github follow-up resolution separately from orphan approvals", () => { @@ -657,7 +633,7 @@ describe("SlackInterface formatting helpers", () => { action: "cleared", }, }, - {} + {}, ); expect(message.text).toBe("GitHub follow-up cleared for task task-123."); @@ -669,9 +645,7 @@ describe("SlackInterface formatting helpers", () => { const blocks = slack.buildApprovalProcessingBlocks("task-123", "retry"); - expect(blocks[0]?.text?.text).toContain( - "Processing approval for task task-123" - ); + expect(blocks[0]?.text?.text).toContain("Processing approval for task task-123"); expect(blocks[0]?.text?.text).toContain("retry"); }); @@ -682,23 +656,17 @@ describe("SlackInterface formatting helpers", () => { "task-123", "retry", "ready_for_prompt", - "worker-123" + "worker-123", ); expect(blocks[0]?.text?.text).toContain("Action: `retry`"); - expect(blocks[0]?.text?.text).toContain( - "Worker: ready_for_prompt (worker-123)" - ); + expect(blocks[0]?.text?.text).toContain("Worker: ready_for_prompt (worker-123)"); }); it("builds approval resolved blocks with only worker status", () => { const slack = createSlackInterface(); - const blocks = slack.buildApprovalResolvedBlocks( - "task-123", - "cancel", - "cancelled" - ); + const blocks = slack.buildApprovalResolvedBlocks("task-123", "cancel", "cancelled"); expect(blocks[0]?.text?.text).toContain("Action: `cancel`"); expect(blocks[0]?.text?.text).toContain("Worker: cancelled"); @@ -707,10 +675,7 @@ describe("SlackInterface formatting helpers", () => { it("builds approval resolved blocks without worker details", () => { const slack = createSlackInterface(); - const blocks = slack.buildApprovalResolvedBlocks( - "task-123", - "already_resolved" - ); + const blocks = slack.buildApprovalResolvedBlocks("task-123", "already_resolved"); expect(blocks[0]?.text?.text).toContain("Action: `already_resolved`"); expect(blocks[0]?.text?.text).not.toContain("Worker:"); @@ -719,10 +684,7 @@ describe("SlackInterface formatting helpers", () => { it("builds approval error blocks with retry and cancel actions", () => { const slack = createSlackInterface(); - const blocks = slack.buildApprovalErrorBlocks( - "task-123", - new Error("provider failed") - ); + const blocks = slack.buildApprovalErrorBlocks("task-123", new Error("provider failed")); const actions = blocks.find((block) => block.type === "actions"); expect(blocks[0]?.text?.text).toContain("provider failed"); @@ -769,10 +731,7 @@ describe("SlackInterface formatting helpers", () => { "orphaned_hosted_agent.retry", "orphaned_hosted_agent.cancel", ]); - expect(actions?.elements?.map((element) => element.value)).toEqual([ - "task-123", - "task-123", - ]); + expect(actions?.elements?.map((element) => element.value)).toEqual(["task-123", "task-123"]); }); it("formats non-orphan approval requests as a simple waiting message", () => { @@ -798,7 +757,7 @@ describe("SlackInterface formatting helpers", () => { repository: "frontal-code/slack", }, }, - task + task, ); expect(message).toEqual({ @@ -829,21 +788,18 @@ describe("SlackInterface formatting helpers", () => { repository: "frontal-code/slack", }, }, - task + task, ); expect(message.text).toBe( - "Task task-123 (frontal-code/slack) needs follow-up: GitHub review requested changes." + "Task task-123 (frontal-code/slack) needs follow-up: GitHub review requested changes.", ); const actions = message.blocks?.find((block) => block.type === "actions"); expect(actions?.elements?.map((element) => element.action_id)).toEqual([ "github_review_followup.ack", "github_review_followup.retry", ]); - expect(actions?.elements?.map((element) => element.value)).toEqual([ - "task-123", - "task-123", - ]); + expect(actions?.elements?.map((element) => element.value)).toEqual(["task-123", "task-123"]); }); it("formats blocked lane events with orphan policy detail in the message text", () => { @@ -875,14 +831,14 @@ describe("SlackInterface formatting helpers", () => { }, }, }, - task + task, ); expect(message?.text).toContain( - "Task task-123 (frontal-code/slack) is blocked: executor heartbeat expired" + "Task task-123 (frontal-code/slack) is blocked: executor heartbeat expired", ); expect(message?.text).toContain( - "Policy: rule (repo=frontal-code/slack); approval 60s, auto-cancel 300s." + "Policy: rule (repo=frontal-code/slack); approval 60s, auto-cancel 300s.", ); }); @@ -914,7 +870,7 @@ describe("SlackInterface formatting helpers", () => { }, }, }, - task + task, ); expect(message).toEqual({ @@ -943,7 +899,7 @@ describe("SlackInterface formatting helpers", () => { channel_id: "C123", }, }, - task + task, ); expect(message).toEqual({ @@ -970,8 +926,8 @@ describe("SlackInterface formatting helpers", () => { emittedAt: "2026-04-09T10:00:09Z", task_id: "task-123", }, - task - ) + task, + ), ).toBeNull(); expect( @@ -988,8 +944,8 @@ describe("SlackInterface formatting helpers", () => { type: "reaction_added", }, }, - task - ) + task, + ), ).toBeNull(); }); @@ -1020,7 +976,7 @@ describe("SlackInterface formatting helpers", () => { }, }, }, - task + task, ); expect(prUpdate).toEqual({ @@ -1046,7 +1002,7 @@ describe("SlackInterface formatting helpers", () => { }, }, }, - task + task, ); expect(merged).toEqual({ @@ -1072,7 +1028,7 @@ describe("SlackInterface formatting helpers", () => { }, }, }, - task + task, ); expect(review).toEqual({ @@ -1097,7 +1053,7 @@ describe("SlackInterface formatting helpers", () => { }, }, }, - task + task, ); expect(comment).toEqual({ @@ -1126,7 +1082,7 @@ describe("SlackInterface formatting helpers", () => { repository: "frontal-code/slack", }, }, - task + task, ); expect(message).toEqual({ @@ -1152,9 +1108,7 @@ describe("SlackInterface formatting helpers", () => { ], }); - expect(response.blocks[1]?.text?.text).toContain( - "1. source=slack -> inherit defaults" - ); + expect(response.blocks[1]?.text?.text).toContain("1. source=slack -> inherit defaults"); }); it("falls back to the default lane failure message when no error details are present", () => { @@ -1178,7 +1132,7 @@ describe("SlackInterface formatting helpers", () => { channel_id: "C123", }, }, - task + task, ); expect(message).toEqual({ @@ -1211,7 +1165,7 @@ describe("SlackInterface formatting helpers", () => { worker_id: "worker-123", }, }, - task + task, ); expect(message).toEqual({ @@ -1240,7 +1194,7 @@ describe("SlackInterface formatting helpers", () => { channel_id: "C123", }, }, - task + task, ); expect(message).toEqual({ @@ -1262,7 +1216,7 @@ describe("SlackInterface formatting helpers", () => { { taskId: "task-123", channelId: "C123", - } + }, ); const invalidStatusMessage = slack.formatFrontalCodeEvent( @@ -1281,7 +1235,7 @@ describe("SlackInterface formatting helpers", () => { { taskId: "task-123", channelId: "C123", - } + }, ); expect(noPayloadMessage).toEqual({ diff --git a/extensions/frontal-code-slack/tests/types/slack-types.test.ts b/extensions/frontal-code-slack/tests/types/slack-types.test.ts index a77e7eb..9f4471b 100644 --- a/extensions/frontal-code-slack/tests/types/slack-types.test.ts +++ b/extensions/frontal-code-slack/tests/types/slack-types.test.ts @@ -328,9 +328,7 @@ describe("Slack Types", () => { expect(typeof user.id).toBe("string"); expect(typeof user.name).toBe("string"); expect(typeof user.real_name).toBe("string"); - expect(user.email === undefined || typeof user.email === "string").toBe( - true - ); + expect(user.email === undefined || typeof user.email === "string").toBe(true); }); it("should validate SlackCommand structure at runtime", () => { @@ -389,8 +387,7 @@ describe("Slack Types", () => { ], }; - const interaction = - data as import("@/types/slack-types").SlackInteraction; + const interaction = data as import("@/types/slack-types").SlackInteraction; expect(typeof interaction.type).toBe("string"); expect(typeof interaction.token).toBe("string"); diff --git a/extensions/frontal-code-slack/tests/utils/env.test.ts b/extensions/frontal-code-slack/tests/utils/env.test.ts index ceda956..fa8b87d 100644 --- a/extensions/frontal-code-slack/tests/utils/env.test.ts +++ b/extensions/frontal-code-slack/tests/utils/env.test.ts @@ -18,11 +18,11 @@ describe("Environment Variables", () => { }); it("should have Frontal Code API configuration", () => { - expect(env.FCODE_API_URL).toBeDefined(); - expect(env.FCODE_API_URL).toMatch(/^https?:\/\//); - expect(env.FCODE_API_TIMEOUT).toBeDefined(); - expect(env.FCODE_API_TIMEOUT).toBeGreaterThanOrEqual(1000); - expect(env.FCODE_API_TIMEOUT).toBeLessThanOrEqual(300000); + expect(env.FRONTAL_CODE_API_URL).toBeDefined(); + expect(env.FRONTAL_CODE_API_URL).toMatch(/^https?:\/\//); + expect(env.FRONTAL_CODE_API_TIMEOUT).toBeDefined(); + expect(env.FRONTAL_CODE_API_TIMEOUT).toBeGreaterThanOrEqual(1000); + expect(env.FRONTAL_CODE_API_TIMEOUT).toBeLessThanOrEqual(300000); }); it("should have database and Redis URLs", () => { @@ -36,9 +36,7 @@ describe("Environment Variables", () => { expect(env.NODE_ENV).toBeDefined(); expect(["development", "production", "test"]).toContain(env.NODE_ENV); expect(env.LOG_LEVEL).toBeDefined(); - expect(["error", "warn", "info", "http", "debug"]).toContain( - env.LOG_LEVEL - ); + expect(["error", "warn", "info", "http", "debug"]).toContain(env.LOG_LEVEL); expect(env.PORT).toBeDefined(); expect(env.PORT).toBeGreaterThanOrEqual(1000); expect(env.PORT).toBeLessThanOrEqual(65535); @@ -58,9 +56,7 @@ describe("Environment Variables", () => { it("should have optional GitHub token", () => { // GitHub token is optional, so it can be undefined - expect( - env.GITHUB_TOKEN === undefined || typeof env.GITHUB_TOKEN === "string" - ).toBe(true); + expect(env.GITHUB_TOKEN === undefined || typeof env.GITHUB_TOKEN === "string").toBe(true); }); }); @@ -101,8 +97,8 @@ describe("Environment Variables", () => { expect(config.slack.appToken).toBe(env.SLACK_APP_TOKEN); expect(config.slack.signingSecret).toBe(env.SLACK_SIGNING_SECRET); - expect(config.frontalCode.apiUrl).toBe(env.FCODE_API_URL); - expect(config.frontalCode.timeout).toBe(env.FCODE_API_TIMEOUT); + expect(config.frontalCode.apiUrl).toBe(env.FRONTAL_CODE_API_URL); + expect(config.frontalCode.timeout).toBe(env.FRONTAL_CODE_API_TIMEOUT); expect(config.database.url).toBe(env.DATABASE_URL); expect(config.redis.url).toBe(env.REDIS_URL); @@ -141,8 +137,8 @@ describe("Environment Variables", () => { describe("Default Values", () => { it("should use default values when environment variables are not set", () => { // These are set in the test setup, but we can verify they match expected defaults - expect(env.FCODE_API_URL).toBe("http://localhost:8787"); - expect(env.FCODE_API_TIMEOUT).toBe(30000); + expect(env.FRONTAL_CODE_API_URL).toBe("http://localhost:8787"); + expect(env.FRONTAL_CODE_API_TIMEOUT).toBe(30000); expect(env.NODE_ENV).toBe("test"); expect(env.LOG_LEVEL).toBe("error"); expect(env.PORT).toBe(3000); @@ -157,8 +153,8 @@ describe("Environment Variables", () => { expect(typeof env.SLACK_BOT_TOKEN).toBe("string"); expect(typeof env.SLACK_APP_TOKEN).toBe("string"); expect(typeof env.SLACK_SIGNING_SECRET).toBe("string"); - expect(typeof env.FCODE_API_URL).toBe("string"); - expect(typeof env.FCODE_API_TIMEOUT).toBe("number"); + expect(typeof env.FRONTAL_CODE_API_URL).toBe("string"); + expect(typeof env.FRONTAL_CODE_API_TIMEOUT).toBe("number"); expect(typeof env.DATABASE_URL).toBe("string"); expect(typeof env.REDIS_URL).toBe("string"); expect(typeof env.NODE_ENV).toBe("string"); @@ -167,9 +163,7 @@ describe("Environment Variables", () => { expect(typeof env.MAX_CONCURRENT_TASKS).toBe("number"); expect(typeof env.TASK_TIMEOUT).toBe("number"); expect(typeof env.HEALTH_CHECK_INTERVAL).toBe("number"); - expect( - env.GITHUB_TOKEN === undefined || typeof env.GITHUB_TOKEN === "string" - ).toBe(true); + expect(env.GITHUB_TOKEN === undefined || typeof env.GITHUB_TOKEN === "string").toBe(true); }); }); @@ -180,14 +174,14 @@ describe("Environment Variables", () => { }); it("should validate URL formats", () => { - expect(env.FCODE_API_URL).toMatch(/^https?:\/\/.+/); + expect(env.FRONTAL_CODE_API_URL).toMatch(/^https?:\/\/.+/); expect(env.DATABASE_URL).toMatch(/^postgresql:\/\/.+/); expect(env.REDIS_URL).toMatch(/^redis:\/\/.+/); }); it("should validate numeric ranges", () => { - expect(env.FCODE_API_TIMEOUT).toBeGreaterThanOrEqual(1000); - expect(env.FCODE_API_TIMEOUT).toBeLessThanOrEqual(300000); + expect(env.FRONTAL_CODE_API_TIMEOUT).toBeGreaterThanOrEqual(1000); + expect(env.FRONTAL_CODE_API_TIMEOUT).toBeLessThanOrEqual(300000); expect(env.PORT).toBeGreaterThanOrEqual(1000); expect(env.PORT).toBeLessThanOrEqual(65535); expect(env.MAX_CONCURRENT_TASKS).toBeGreaterThanOrEqual(1); @@ -200,9 +194,7 @@ describe("Environment Variables", () => { it("should validate enum values", () => { expect(["development", "production", "test"]).toContain(env.NODE_ENV); - expect(["error", "warn", "info", "http", "debug"]).toContain( - env.LOG_LEVEL - ); + expect(["error", "warn", "info", "http", "debug"]).toContain(env.LOG_LEVEL); }); }); }); diff --git a/extensions/frontal-code-slack/tests/utils/validators.test.ts b/extensions/frontal-code-slack/tests/utils/validators.test.ts index 4686534..0a86812 100644 --- a/extensions/frontal-code-slack/tests/utils/validators.test.ts +++ b/extensions/frontal-code-slack/tests/utils/validators.test.ts @@ -15,10 +15,7 @@ import { validateTaskCreationRequest, validateUserPreferences, } from "@/utils/validators"; -import { - createMockSlackCommand, - createMockTaskCreationRequest, -} from "../setup"; +import { createMockSlackCommand, createMockTaskCreationRequest } from "../setup"; describe("Validators", () => { describe("validateSlackCommand", () => { @@ -235,12 +232,7 @@ describe("Validators", () => { }); it("should reject invalid branch names", () => { - const invalidNames = [ - "branch with spaces", - "branch@with$symbols", - "branch#with#hash", - "", - ]; + const invalidNames = ["branch with spaces", "branch@with$symbols", "branch#with#hash", ""]; invalidNames.forEach((name) => { const result = validateBranchName(name); @@ -390,10 +382,7 @@ describe("Validators", () => { describe("isValidSlackAppToken", () => { it("should validate valid Slack app tokens", () => { - const validTokens = [ - "xapp-1-1234567890-ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "xapp-A-1-A", - ]; + const validTokens = ["xapp-1-1234567890-ABCDEFGHIJKLMNOPQRSTUVWXYZ", "xapp-A-1-A"]; validTokens.forEach((token) => { expect(isValidSlackAppToken(token)).toBe(true); @@ -417,11 +406,7 @@ describe("Validators", () => { describe("isValidSlackSigningSecret", () => { it("should validate valid signing secrets", () => { - const validSecrets = [ - "a".repeat(32), - "a".repeat(64), - "abcdefghijklmnopqrstuvwxyz123456", - ]; + const validSecrets = ["a".repeat(32), "a".repeat(64), "abcdefghijklmnopqrstuvwxyz123456"]; validSecrets.forEach((secret) => { expect(isValidSlackSigningSecret(secret)).toBe(true); diff --git a/extensions/frontal-code-slack/tests/validators.test.ts b/extensions/frontal-code-slack/tests/validators.test.ts index c8405d8..38bdf8d 100644 --- a/extensions/frontal-code-slack/tests/validators.test.ts +++ b/extensions/frontal-code-slack/tests/validators.test.ts @@ -179,10 +179,7 @@ describe("Validators", () => { describe("isValidSlackAppToken", () => { it("should validate valid Slack app tokens", () => { - const validTokens = [ - "xapp-1-1234567890-ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "xapp-A-1-A", - ]; + const validTokens = ["xapp-1-1234567890-ABCDEFGHIJKLMNOPQRSTUVWXYZ", "xapp-A-1-A"]; validTokens.forEach((token) => { expect(isValidSlackAppToken(token)).toBe(true); @@ -206,11 +203,7 @@ describe("Validators", () => { describe("isValidSlackSigningSecret", () => { it("should validate valid signing secrets", () => { - const validSecrets = [ - "a".repeat(32), - "a".repeat(64), - "abcdefghijklmnopqrstuvwxyz123456", - ]; + const validSecrets = ["a".repeat(32), "a".repeat(64), "abcdefghijklmnopqrstuvwxyz123456"]; validSecrets.forEach((secret) => { expect(isValidSlackSigningSecret(secret)).toBe(true); diff --git a/extensions/frontal-code-slack/vitest.config.ts b/extensions/frontal-code-slack/vitest.config.ts index faefda2..87b6de5 100644 --- a/extensions/frontal-code-slack/vitest.config.ts +++ b/extensions/frontal-code-slack/vitest.config.ts @@ -47,12 +47,6 @@ export default defineConfig({ "tests/slack-constructor.test.ts", "tests/slack-formatting.test.ts", "tests/slack-behavior.test.ts", - "tests/bot.test.ts", - "tests/validators.test.ts", - "tests/utils/env.test.ts", - "tests/utils/validators.test.ts", - "tests/integration/slack-workflow.test.ts", - "tests/bot/slack-bot-service.test.ts", ], // Exclude patterns diff --git a/fly-slack.toml b/fly-slack.toml index c5764b2..769d995 100644 --- a/fly-slack.toml +++ b/fly-slack.toml @@ -8,7 +8,7 @@ primary_region = "iad" NODE_ENV = "production" LOG_LEVEL = "info" PORT = "3000" - FCODE_API_URL = "https://frontal-code.fly.dev" + FRONTAL_CODE_API_URL = "https://frontal-code.fly.dev" [http_service] internal_port = 3000 diff --git a/fly.toml b/fly.toml index fb3e914..cc0d5a2 100644 --- a/fly.toml +++ b/fly.toml @@ -5,13 +5,13 @@ primary_region = "iad" dockerfile = "infrastructure/docker/frontal-code-fly.Dockerfile" [env] - FCODE_SERVER_HOST = "0.0.0.0" - FCODE_SERVER_PORT = "8788" - FCODE_SERVER_LANE_TRANSPORT = "tools-agent" - FCODE_SERVER_STATE_FILE = "/var/lib/frontal-code/server/state.json" - FCODE_SERVER_WORKSPACE_ROOT = "/workspace/workspaces" - FCODE_SERVER_RECONCILE_INTERVAL_SECS = "15" - FCODE_AGENT_STORE = "/var/lib/frontal-code/agents" + FRONTAL_CODE_SERVER_HOST = "0.0.0.0" + FRONTAL_CODE_SERVER_PORT = "8788" + FRONTAL_CODE_SERVER_LANE_TRANSPORT = "tools-agent" + FRONTAL_CODE_SERVER_STATE_FILE = "/var/lib/frontal-code/server/state.json" + FRONTAL_CODE_SERVER_WORKSPACE_ROOT = "/workspace/workspaces" + FRONTAL_CODE_SERVER_RECONCILE_INTERVAL_SECS = "15" + FRONTAL_CODE_AGENT_STORE = "/var/lib/frontal-code/agents" RUST_LOG = "info" [http_service] diff --git a/infrastructure/README.md b/infrastructure/README.md index 5456657..576aa79 100644 --- a/infrastructure/README.md +++ b/infrastructure/README.md @@ -15,7 +15,7 @@ The configuration is split into: ## Assumptions - The current production surface is `frontal-code-server` on port `8788`. -- The Slack connector talks to the server via `FCODE_API_URL`. +- The Slack connector talks to the server via `FRONTAL_CODE_API_URL`. - Slack runs in Socket Mode, so it does not require public inbound traffic from Slack. - Hosted-agent state should survive restarts. - Code and repository changes, when performed by hosted agents, happen relative to the server container working directory. This stack mounts a writable shared workspace at `/workspace` for that purpose. @@ -39,7 +39,7 @@ The configuration is split into: 1. Copy `compose/.env.example` to `compose/.env`. 2. Fill in Slack credentials and at least one provider key. -3. (Optional) To enable Linear/Graphite tracking: set `FCODE_LINEAR_API_TOKEN` / `FCODE_GRAPHITE_API_TOKEN` and, if you enforce webhook signatures, `FCODE_LINEAR_WEBHOOK_SECRET` / `FCODE_GRAPHITE_WEBHOOK_SECRET`. +3. (Optional) To enable Linear/Graphite tracking: set `FRONTAL_CODE_LINEAR_API_TOKEN` / `FRONTAL_CODE_GRAPHITE_API_TOKEN` and, if you enforce webhook signatures, `FRONTAL_CODE_LINEAR_WEBHOOK_SECRET` / `FRONTAL_CODE_GRAPHITE_WEBHOOK_SECRET`. 4. Set `FRONTAL_SERVER_API_KEY` in `compose/.env`, and set the same shared secret as `FRONTAL_SERVER_API_KEY` for any connector calling the hosted control plane. 5. Start the stack: @@ -49,14 +49,14 @@ docker compose --env-file infrastructure/compose/.env -f infrastructure/compose/ ``` By default the main API is published on `http://127.0.0.1:8788`. Override -`FCODE_SERVER_PUBLISH_ADDR` in `compose/.env` only when you intentionally need a wider bind. +`FRONTAL_CODE_SERVER_PUBLISH_ADDR` in `compose/.env` only when you intentionally need a wider bind. ### Local Docker Worker Mode To run hosted tasks inside sibling Docker worker containers: -1. Set `FCODE_SERVER_LANE_TRANSPORT=local-docker` in `compose/.env`. -2. If worker callbacks must reach the published port, set `FCODE_SERVER_PUBLISH_ADDR=0.0.0.0` +1. Set `FRONTAL_CODE_SERVER_LANE_TRANSPORT=local-docker` in `compose/.env`. +2. If worker callbacks must reach the published port, set `FRONTAL_CODE_SERVER_PUBLISH_ADDR=0.0.0.0` in `compose/.env`. 3. Build the worker image: @@ -76,9 +76,9 @@ docker compose --env-file infrastructure/compose/.env \ In this mode: - `frontal-code-server` uses `/var/run/docker.sock` to launch worker containers -- workers default to `FCODE_SERVER_DOCKER_IMAGE=frontal-code-worker:local` -- workers call back to the server through `FCODE_SERVER_CALLBACK_URL` -- per-task repo checkouts live under `FCODE_SERVER_WORKSPACE_ROOT` +- workers default to `FRONTAL_CODE_SERVER_DOCKER_IMAGE=frontal-code-worker:local` +- workers call back to the server through `FRONTAL_CODE_SERVER_CALLBACK_URL` +- per-task repo checkouts live under `FRONTAL_CODE_SERVER_WORKSPACE_ROOT` ## Kubernetes diff --git a/infrastructure/compose/.env.example b/infrastructure/compose/.env.example index e25065c..74f0178 100644 --- a/infrastructure/compose/.env.example +++ b/infrastructure/compose/.env.example @@ -1,28 +1,28 @@ # Frontal Code server -FCODE_SERVER_HOST=0.0.0.0 -FCODE_SERVER_PORT=8788 -FCODE_SERVER_PUBLISH_ADDR=127.0.0.1 -FCODE_SERVER_LANE_TRANSPORT=tools-agent -FCODE_SERVER_API_KEY=replace-with-a-long-random-api-key -FCODE_SERVER_WORKSPACE_ROOT=/workspace/workspaces -FCODE_SERVER_DOCKER_IMAGE=frontal-code-worker:local -# Set FCODE_SERVER_PUBLISH_ADDR=0.0.0.0 if external hosts or Linux worker callbacks must reach the published port. -FCODE_SERVER_CALLBACK_URL=http://host.docker.internal:8788 -FCODE_SERVER_RECONCILE_INTERVAL_SECS=15 -FCODE_SERVER_ORPHAN_APPROVAL_DELAY_SECS=0 -FCODE_SERVER_ORPHAN_AUTO_RETRY_SECS=0 -FCODE_SERVER_ORPHAN_AUTO_CANCEL_SECS=0 -FCODE_SERVER_ORPHAN_POLICY_RULES=[] -FCODE_SERVER_STATE_FILE=/var/lib/frontal-code/server/state.json -FCODE_AGENT_STORE=/var/lib/frontal-code/agents +FRONTAL_CODE_SERVER_HOST=0.0.0.0 +FRONTAL_CODE_SERVER_PORT=8788 +FRONTAL_CODE_SERVER_PUBLISH_ADDR=127.0.0.1 +FRONTAL_CODE_SERVER_LANE_TRANSPORT=tools-agent +FRONTAL_CODE_SERVER_API_KEY=replace-with-a-long-random-api-key +FRONTAL_CODE_SERVER_WORKSPACE_ROOT=/workspace/workspaces +FRONTAL_CODE_SERVER_DOCKER_IMAGE=frontal-code-worker:local +# Set FRONTAL_CODE_SERVER_PUBLISH_ADDR=0.0.0.0 if external hosts or Linux worker callbacks must reach the published port. +FRONTAL_CODE_SERVER_CALLBACK_URL=http://host.docker.internal:8788 +FRONTAL_CODE_SERVER_RECONCILE_INTERVAL_SECS=15 +FRONTAL_CODE_SERVER_ORPHAN_APPROVAL_DELAY_SECS=0 +FRONTAL_CODE_SERVER_ORPHAN_AUTO_RETRY_SECS=0 +FRONTAL_CODE_SERVER_ORPHAN_AUTO_CANCEL_SECS=0 +FRONTAL_CODE_SERVER_ORPHAN_POLICY_RULES=[] +FRONTAL_CODE_SERVER_STATE_FILE=/var/lib/frontal-code/server/state.json +FRONTAL_CODE_AGENT_STORE=/var/lib/frontal-code/agents RUST_LOG=info -FCODE_GITHUB_WEBHOOK_SECRET= -FCODE_LINEAR_API_TOKEN= -FCODE_LINEAR_API_URL=https://api.linear.app/graphql -FCODE_LINEAR_WEBHOOK_SECRET= -FCODE_GRAPHITE_API_TOKEN= -FCODE_GRAPHITE_API_URL=https://graphite.dev/api -FCODE_GRAPHITE_WEBHOOK_SECRET= +FRONTAL_CODE_GITHUB_WEBHOOK_SECRET= +FRONTAL_CODE_LINEAR_API_TOKEN= +FRONTAL_CODE_LINEAR_API_URL=https://api.linear.app/graphql +FRONTAL_CODE_LINEAR_WEBHOOK_SECRET= +FRONTAL_CODE_GRAPHITE_API_TOKEN= +FRONTAL_CODE_GRAPHITE_API_URL=https://graphite.dev/api +FRONTAL_CODE_GRAPHITE_WEBHOOK_SECRET= # AI providers FRONTAL_API_KEY= @@ -41,8 +41,8 @@ OLLAMA_BASE_URL= OLLAMA_MODEL= # Slack connector -FCODE_API_URL=http://frontal-code-server:8788 -FCODE_API_TIMEOUT=30000 +FRONTAL_CODE_API_URL=http://frontal-code-server:8788 +FRONTAL_CODE_API_TIMEOUT=30000 FRONTAL_SERVER_API_KEY=replace-with-the-same-frontal-code-server-api-key SLACK_BOT_TOKEN=xoxb-your-bot-token SLACK_APP_TOKEN=xapp-your-app-token diff --git a/infrastructure/compose/docker-compose.docker-worker.yml b/infrastructure/compose/docker-compose.docker-worker.yml index ab633c5..1bcf62c 100644 --- a/infrastructure/compose/docker-compose.docker-worker.yml +++ b/infrastructure/compose/docker-compose.docker-worker.yml @@ -1,6 +1,6 @@ services: frontal-code-server: environment: - FCODE_SERVER_LANE_TRANSPORT: ${FCODE_SERVER_LANE_TRANSPORT:-local-docker} + FRONTAL_CODE_SERVER_LANE_TRANSPORT: ${FRONTAL_CODE_SERVER_LANE_TRANSPORT:-local-docker} volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/infrastructure/compose/docker-compose.yml b/infrastructure/compose/docker-compose.yml index 95a5eb7..4338a49 100644 --- a/infrastructure/compose/docker-compose.yml +++ b/infrastructure/compose/docker-compose.yml @@ -6,22 +6,22 @@ services: image: frontal-code-server:local restart: unless-stopped environment: - FCODE_SERVER_HOST: ${FCODE_SERVER_HOST:-0.0.0.0} - FCODE_SERVER_PORT: ${FCODE_SERVER_PORT:-8788} - FCODE_SERVER_LANE_TRANSPORT: ${FCODE_SERVER_LANE_TRANSPORT:-tools-agent} - FCODE_SERVER_API_KEY: ${FCODE_SERVER_API_KEY:-} - FCODE_SERVER_WORKSPACE_ROOT: ${FCODE_SERVER_WORKSPACE_ROOT:-/workspace/workspaces} - FCODE_SERVER_DOCKER_IMAGE: ${FCODE_SERVER_DOCKER_IMAGE:-frontal-code-worker:local} - FCODE_SERVER_CALLBACK_URL: ${FCODE_SERVER_CALLBACK_URL:-http://host.docker.internal:8788} - FCODE_SERVER_RECONCILE_INTERVAL_SECS: ${FCODE_SERVER_RECONCILE_INTERVAL_SECS:-15} - FCODE_SERVER_ORPHAN_APPROVAL_DELAY_SECS: ${FCODE_SERVER_ORPHAN_APPROVAL_DELAY_SECS:-0} - FCODE_SERVER_ORPHAN_AUTO_RETRY_SECS: ${FCODE_SERVER_ORPHAN_AUTO_RETRY_SECS:-0} - FCODE_SERVER_ORPHAN_AUTO_CANCEL_SECS: ${FCODE_SERVER_ORPHAN_AUTO_CANCEL_SECS:-0} - FCODE_SERVER_ORPHAN_POLICY_RULES: ${FCODE_SERVER_ORPHAN_POLICY_RULES:-[]} - FCODE_SERVER_STATE_FILE: ${FCODE_SERVER_STATE_FILE:-/var/lib/frontal-code/server/state.json} - FCODE_AGENT_STORE: ${FCODE_AGENT_STORE:-/var/lib/frontal-code/agents} + FRONTAL_CODE_SERVER_HOST: ${FRONTAL_CODE_SERVER_HOST:-0.0.0.0} + FRONTAL_CODE_SERVER_PORT: ${FRONTAL_CODE_SERVER_PORT:-8788} + FRONTAL_CODE_SERVER_LANE_TRANSPORT: ${FRONTAL_CODE_SERVER_LANE_TRANSPORT:-tools-agent} + FRONTAL_CODE_SERVER_API_KEY: ${FRONTAL_CODE_SERVER_API_KEY:-} + FRONTAL_CODE_SERVER_WORKSPACE_ROOT: ${FRONTAL_CODE_SERVER_WORKSPACE_ROOT:-/workspace/workspaces} + FRONTAL_CODE_SERVER_DOCKER_IMAGE: ${FRONTAL_CODE_SERVER_DOCKER_IMAGE:-frontal-code-worker:local} + FRONTAL_CODE_SERVER_CALLBACK_URL: ${FRONTAL_CODE_SERVER_CALLBACK_URL:-http://host.docker.internal:8788} + FRONTAL_CODE_SERVER_RECONCILE_INTERVAL_SECS: ${FRONTAL_CODE_SERVER_RECONCILE_INTERVAL_SECS:-15} + FRONTAL_CODE_SERVER_ORPHAN_APPROVAL_DELAY_SECS: ${FRONTAL_CODE_SERVER_ORPHAN_APPROVAL_DELAY_SECS:-0} + FRONTAL_CODE_SERVER_ORPHAN_AUTO_RETRY_SECS: ${FRONTAL_CODE_SERVER_ORPHAN_AUTO_RETRY_SECS:-0} + FRONTAL_CODE_SERVER_ORPHAN_AUTO_CANCEL_SECS: ${FRONTAL_CODE_SERVER_ORPHAN_AUTO_CANCEL_SECS:-0} + FRONTAL_CODE_SERVER_ORPHAN_POLICY_RULES: ${FRONTAL_CODE_SERVER_ORPHAN_POLICY_RULES:-[]} + FRONTAL_CODE_SERVER_STATE_FILE: ${FRONTAL_CODE_SERVER_STATE_FILE:-/var/lib/frontal-code/server/state.json} + FRONTAL_CODE_AGENT_STORE: ${FRONTAL_CODE_AGENT_STORE:-/var/lib/frontal-code/agents} RUST_LOG: ${RUST_LOG:-info} - FCODE_GITHUB_WEBHOOK_SECRET: ${FCODE_GITHUB_WEBHOOK_SECRET:-} + FRONTAL_CODE_GITHUB_WEBHOOK_SECRET: ${FRONTAL_CODE_GITHUB_WEBHOOK_SECRET:-} FRONTAL_API_KEY: ${FRONTAL_API_KEY:-} FRONTAL_AUTH_TOKEN: ${FRONTAL_AUTH_TOKEN:-} FRONTAL_BASE_URL: ${FRONTAL_BASE_URL:-} @@ -47,7 +47,7 @@ services: cap_drop: - ALL ports: - - "${FCODE_SERVER_PUBLISH_ADDR:-127.0.0.1}:${FCODE_SERVER_PORT:-8788}:8788" + - "${FRONTAL_CODE_SERVER_PUBLISH_ADDR:-127.0.0.1}:${FRONTAL_CODE_SERVER_PORT:-8788}:8788" healthcheck: test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8788/health"] interval: 30s @@ -71,8 +71,8 @@ services: image: frontal-code-slack:local restart: unless-stopped environment: - FCODE_API_URL: ${FCODE_API_URL:-http://frontal-code-server:8788} - FCODE_API_TIMEOUT: ${FCODE_API_TIMEOUT:-30000} + FRONTAL_CODE_API_URL: ${FRONTAL_CODE_API_URL:-http://frontal-code-server:8788} + FRONTAL_CODE_API_TIMEOUT: ${FRONTAL_CODE_API_TIMEOUT:-30000} FRONTAL_SERVER_API_KEY: ${FRONTAL_SERVER_API_KEY:-} SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN} SLACK_APP_TOKEN: ${SLACK_APP_TOKEN} diff --git a/infrastructure/deploy.sh b/infrastructure/deploy.sh index cb9c577..d2ad6d1 100755 --- a/infrastructure/deploy.sh +++ b/infrastructure/deploy.sh @@ -113,7 +113,7 @@ verify_deployment() { # Get outputs TOOLS_URL=$(terraform output -raw tools_domain_url) - FCODE_URL=$(terraform output -raw frontal-code_tools_url) + FRONTAL_CODE_URL=$(terraform output -raw frontal-code_tools_url) NLB_DNS=$(terraform output -raw nginx_ingress_load_balancer) log_info "Waiting for DNS propagation..." @@ -151,10 +151,10 @@ show_results() { cd "$TERRAFORM_DIR" TOOLS_URL=$(terraform output -raw tools_domain_url) - FCODE_URL=$(terraform output -raw frontal-code_tools_url) + FRONTAL_CODE_URL=$(terraform output -raw frontal-code_tools_url) NLB_DNS=$(terraform output -raw nginx_ingress_load_balancer) - FCODE_SERVER_DEPLOYMENT=$(terraform output -raw frontal-code_server_deployment) - FCODE_SLACK_DEPLOYMENT=$(terraform output -raw frontal-code_slack_deployment) + FRONTAL_CODE_SERVER_DEPLOYMENT=$(terraform output -raw frontal-code_server_deployment) + FRONTAL_CODE_SLACK_DEPLOYMENT=$(terraform output -raw frontal-code_slack_deployment) ENVIRONMENT_INFO=$(terraform output -json environment_info) echo @@ -166,12 +166,12 @@ show_results() { echo echo "URLs:" echo " Tools Landing Page: $TOOLS_URL" - echo " Frontal Code API Endpoint: $FCODE_URL" + echo " Frontal Code API Endpoint: $FRONTAL_CODE_URL" echo " NLB DNS Name: $NLB_DNS" echo echo "Deployments:" - echo " Frontal Code Server: $FCODE_SERVER_DEPLOYMENT" - echo " Frontal Code Slack: $FCODE_SLACK_DEPLOYMENT" + echo " Frontal Code Server: $FRONTAL_CODE_SERVER_DEPLOYMENT" + echo " Frontal Code Slack: $FRONTAL_CODE_SLACK_DEPLOYMENT" echo # Show storage info if frontal-code-server is deployed @@ -195,13 +195,13 @@ show_results() { fi echo "Environment variables for Frontal Code CLI:" - echo "export FRONTAL_BASE_URL=\"$FCODE_URL\"" - echo "export FCODE_HOSTED_CALLBACK_URL=\"$FCODE_URL/webhooks/tasks\"" + echo "export FRONTAL_BASE_URL=\"$FRONTAL_CODE_URL\"" + echo "export FRONTAL_CODE_HOSTED_CALLBACK_URL=\"$FRONTAL_CODE_URL/webhooks/tasks\"" echo echo "Next steps:" echo "1. Update your Frontal Code CLI configuration with the URLs above" echo "2. Test the deployment by visiting: $TOOLS_URL" - echo "3. Check the Frontal Code API health: $FCODE_URL/health" + echo "3. Check the Frontal Code API health: $FRONTAL_CODE_URL/health" echo "4. Verify Slack integration is working (if deployed)" echo "5. Check Kubernetes resources:" echo " kubectl get pods -n $(echo "$ENVIRONMENT_INFO" | jq -r '.namespace')" diff --git a/infrastructure/docker/frontal-code-cli.Dockerfile b/infrastructure/docker/frontal-code-cli.Dockerfile index bc342f6..9b88cc0 100644 --- a/infrastructure/docker/frontal-code-cli.Dockerfile +++ b/infrastructure/docker/frontal-code-cli.Dockerfile @@ -65,7 +65,7 @@ USER frontal-code # Environment variables ENV CARGO_TERM_COLOR=always -ENV FCODE_HOME=/workspace/.frontal-code +ENV FRONTAL_CODE_HOME=/workspace/.frontal-code ENV SANDBOX_HOME=/workspace/.sandbox-home # Health check diff --git a/infrastructure/docker/frontal-code-dev.Dockerfile b/infrastructure/docker/frontal-code-dev.Dockerfile index d8ebe9c..7fab6ee 100644 --- a/infrastructure/docker/frontal-code-dev.Dockerfile +++ b/infrastructure/docker/frontal-code-dev.Dockerfile @@ -30,7 +30,7 @@ USER frontal-code # Environment variables ENV CARGO_TERM_COLOR=always -ENV FCODE_HOME=/workspace/.frontal-code +ENV FRONTAL_CODE_HOME=/workspace/.frontal-code ENV SANDBOX_HOME=/workspace/.sandbox-home # Default command with hot reload diff --git a/infrastructure/docker/frontal-code-worker.Dockerfile b/infrastructure/docker/frontal-code-worker.Dockerfile index 749ddcb..aaeec82 100644 --- a/infrastructure/docker/frontal-code-worker.Dockerfile +++ b/infrastructure/docker/frontal-code-worker.Dockerfile @@ -51,7 +51,7 @@ RUN chmod +x /usr/local/bin/frontal-code \ USER frontal-code ENV CARGO_TERM_COLOR=always -ENV FCODE_HOME=/workspace/.frontal-code +ENV FRONTAL_CODE_HOME=/workspace/.frontal-code ENV SANDBOX_HOME=/workspace/.sandbox-home CMD ["frontal-code", "--version"] diff --git a/infrastructure/kubernetes/base/configmap.yaml b/infrastructure/kubernetes/base/configmap.yaml index 80d86c1..fd6c229 100644 --- a/infrastructure/kubernetes/base/configmap.yaml +++ b/infrastructure/kubernetes/base/configmap.yaml @@ -3,19 +3,19 @@ kind: ConfigMap metadata: name: frontal-code-config data: - FCODE_SERVER_HOST: "0.0.0.0" - FCODE_SERVER_PORT: "8788" - FCODE_SERVER_LANE_TRANSPORT: "tools-agent" - FCODE_SERVER_RECONCILE_INTERVAL_SECS: "15" - FCODE_SERVER_ORPHAN_APPROVAL_DELAY_SECS: "0" - FCODE_SERVER_ORPHAN_AUTO_RETRY_SECS: "0" - FCODE_SERVER_ORPHAN_AUTO_CANCEL_SECS: "0" - FCODE_SERVER_ORPHAN_POLICY_RULES: "[]" - FCODE_SERVER_STATE_FILE: "/var/lib/frontal-code/server/state.json" - FCODE_AGENT_STORE: "/var/lib/frontal-code/agents" + FRONTAL_CODE_SERVER_HOST: "0.0.0.0" + FRONTAL_CODE_SERVER_PORT: "8788" + FRONTAL_CODE_SERVER_LANE_TRANSPORT: "tools-agent" + FRONTAL_CODE_SERVER_RECONCILE_INTERVAL_SECS: "15" + FRONTAL_CODE_SERVER_ORPHAN_APPROVAL_DELAY_SECS: "0" + FRONTAL_CODE_SERVER_ORPHAN_AUTO_RETRY_SECS: "0" + FRONTAL_CODE_SERVER_ORPHAN_AUTO_CANCEL_SECS: "0" + FRONTAL_CODE_SERVER_ORPHAN_POLICY_RULES: "[]" + FRONTAL_CODE_SERVER_STATE_FILE: "/var/lib/frontal-code/server/state.json" + FRONTAL_CODE_AGENT_STORE: "/var/lib/frontal-code/agents" RUST_LOG: "info" - FCODE_API_URL: "http://frontal-code-server:8788" - FCODE_API_TIMEOUT: "30000" + FRONTAL_CODE_API_URL: "http://frontal-code-server:8788" + FRONTAL_CODE_API_TIMEOUT: "30000" NODE_ENV: "production" LOG_LEVEL: "info" PORT: "3000" diff --git a/infrastructure/kubernetes/base/secrets.example.yaml b/infrastructure/kubernetes/base/secrets.example.yaml index 878ff82..6316cf2 100644 --- a/infrastructure/kubernetes/base/secrets.example.yaml +++ b/infrastructure/kubernetes/base/secrets.example.yaml @@ -5,8 +5,8 @@ metadata: namespace: frontal-code type: Opaque stringData: - FCODE_SERVER_API_KEY: "replace-with-a-long-random-api-key" - FCODE_GITHUB_WEBHOOK_SECRET: "" + FRONTAL_CODE_SERVER_API_KEY: "replace-with-a-long-random-api-key" + FRONTAL_CODE_GITHUB_WEBHOOK_SECRET: "" FRONTAL_API_KEY: "" FRONTAL_AUTH_TOKEN: "" FRONTAL_BASE_URL: "" diff --git a/infrastructure/terraform/README.md b/infrastructure/terraform/README.md index 9021c07..d295a88 100644 --- a/infrastructure/terraform/README.md +++ b/infrastructure/terraform/README.md @@ -158,7 +158,7 @@ Update your Frontal Code CLI configuration to use the new URLs: ```bash export FRONTAL_BASE_URL="https://tools.frontal.dev/frontal-code" -export FCODE_HOSTED_CALLBACK_URL="https://tools.frontal.dev/frontal-code/webhooks/tasks" +export FRONTAL_CODE_HOSTED_CALLBACK_URL="https://tools.frontal.dev/frontal-code/webhooks/tasks" ``` ## Security Features diff --git a/infrastructure/terraform/frontal-code-server.tf b/infrastructure/terraform/frontal-code-server.tf index 51e1e95..c7e07c5 100644 --- a/infrastructure/terraform/frontal-code-server.tf +++ b/infrastructure/terraform/frontal-code-server.tf @@ -96,16 +96,16 @@ resource "kubernetes_config_map" "frontal-code_server_config" { } data = { - "FCODE_SERVER_HOST" = "0.0.0.0" - "FCODE_SERVER_PORT" = "8788" - "FCODE_SERVER_LANE_TRANSPORT" = "tools-agent" - "FCODE_SERVER_RECONCILE_INTERVAL_SECS" = "15" - "FCODE_SERVER_ORPHAN_APPROVAL_DELAY_SECS" = "0" - "FCODE_SERVER_ORPHAN_AUTO_RETRY_SECS" = "0" - "FCODE_SERVER_ORPHAN_AUTO_CANCEL_SECS" = "0" - "FCODE_SERVER_ORPHAN_POLICY_RULES" = "[]" - "FCODE_SERVER_STATE_FILE" = "/var/lib/frontal-code/server/state.json" - "FCODE_AGENT_STORE" = "/var/lib/frontal-code/agents" + "FRONTAL_CODE_SERVER_HOST" = "0.0.0.0" + "FRONTAL_CODE_SERVER_PORT" = "8788" + "FRONTAL_CODE_SERVER_LANE_TRANSPORT" = "tools-agent" + "FRONTAL_CODE_SERVER_RECONCILE_INTERVAL_SECS" = "15" + "FRONTAL_CODE_SERVER_ORPHAN_APPROVAL_DELAY_SECS" = "0" + "FRONTAL_CODE_SERVER_ORPHAN_AUTO_RETRY_SECS" = "0" + "FRONTAL_CODE_SERVER_ORPHAN_AUTO_CANCEL_SECS" = "0" + "FRONTAL_CODE_SERVER_ORPHAN_POLICY_RULES" = "[]" + "FRONTAL_CODE_SERVER_STATE_FILE" = "/var/lib/frontal-code/server/state.json" + "FRONTAL_CODE_AGENT_STORE" = "/var/lib/frontal-code/agents" "RUST_LOG" = "info" } @@ -125,7 +125,7 @@ resource "kubernetes_secret" "frontal-code_server_secrets" { } data = { - "FCODE_SERVER_API_KEY" = var.frontal-code_server_api_key + "FRONTAL_CODE_SERVER_API_KEY" = var.frontal-code_server_api_key "FRONTAL_API_KEY" = var.api_keys.anthropic "FRONTAL_BASE_URL" = "https://tools.frontal.dev/frontal-code" "OPENAI_API_KEY" = var.api_keys.openai diff --git a/infrastructure/terraform/frontal-code-slack.tf b/infrastructure/terraform/frontal-code-slack.tf index e39b34b..bdcba07 100644 --- a/infrastructure/terraform/frontal-code-slack.tf +++ b/infrastructure/terraform/frontal-code-slack.tf @@ -11,8 +11,8 @@ resource "kubernetes_config_map" "frontal-code_slack_config" { } data = { - "FCODE_API_URL" = "http://frontal-code-server:8788" - "FCODE_API_TIMEOUT" = "30000" + "FRONTAL_CODE_API_URL" = "http://frontal-code-server:8788" + "FRONTAL_CODE_API_TIMEOUT" = "30000" "NODE_ENV" = var.environment "LOG_LEVEL" = "info" "PORT" = "3000" diff --git a/scripts/BUILD.bazel b/scripts/BUILD.bazel index fd46116..b3b9490 100644 --- a/scripts/BUILD.bazel +++ b/scripts/BUILD.bazel @@ -31,6 +31,7 @@ exports_files([ sh_test( name = "third_party_check", srcs = ["third_party_check.sh"], + data = ["//:MODULE.bazel", "//third_party:repos.bzl"], tags = ["check"], ) diff --git a/sdks/rust/BUILD.bazel b/sdks/rust/BUILD.bazel index 200440a..ca4928e 100644 --- a/sdks/rust/BUILD.bazel +++ b/sdks/rust/BUILD.bazel @@ -1,4 +1,4 @@ -load("//tools:lint.bzl", "rust_lint", "rust_format") +load("//tools:lint.bzl", "rust_lint") package(default_visibility = ["//visibility:public"]) @@ -14,8 +14,13 @@ filegroup( ) # Format check for the Rust SDK. -rust_format( +sh_test( name = "fmt", + size = "small", + srcs = ["//tools:sdk_rust.sh"], + data = ["//:cargo_workspace"], + args = ["fmt-check"], + tags = ["fmt", "rust"], ) # Clippy (with -D warnings) for the Rust SDK. @@ -28,6 +33,7 @@ rust_lint( sh_test( name = "test", srcs = ["//tools:sdk_rust.sh"], + data = ["//:cargo_workspace"], args = ["test"], tags = ["rust", "test"], ) diff --git a/sdks/rust/tests/common/mod.rs b/sdks/rust/tests/common/mod.rs index 91fd0bf..76082bc 100644 --- a/sdks/rust/tests/common/mod.rs +++ b/sdks/rust/tests/common/mod.rs @@ -32,8 +32,8 @@ impl MockFrontalCode { let script = format!( "#!/usr/bin/env bash\n\ - CAP=\"${{FCODE_MOCK_CAPTURE:-/dev/null}}\"\n\ - CWD=\"${{FCODE_MOCK_CWD:-/dev/null}}\"\n\ + CAP=\"${{FRONTAL_CODE_MOCK_CAPTURE:-/dev/null}}\"\n\ + CWD=\"${{FRONTAL_CODE_MOCK_CWD:-/dev/null}}\"\n\ RESP=\"{resp}\"\n\ EXIT=\"{exit}\"\n\ pwd > \"$CWD\"\n\ @@ -61,11 +61,11 @@ impl MockFrontalCode { pub fn env(&self) -> HashMap { let mut map = HashMap::new(); map.insert( - "FCODE_MOCK_CAPTURE".into(), + "FRONTAL_CODE_MOCK_CAPTURE".into(), self.capture_path.to_string_lossy().into_owned(), ); map.insert( - "FCODE_MOCK_CWD".into(), + "FRONTAL_CODE_MOCK_CWD".into(), self.cwd_path.to_string_lossy().into_owned(), ); map.insert("PATH".into(), std::env::var("PATH").unwrap_or_default()); diff --git a/sdks/typescript/BUILD.bazel b/sdks/typescript/BUILD.bazel index 9fdc623..b2937bb 100644 --- a/sdks/typescript/BUILD.bazel +++ b/sdks/typescript/BUILD.bazel @@ -19,12 +19,14 @@ filegroup( biome_lint( name = "lint", target = "sdks/typescript", + data = [":typescript_sdk_srcs", "//:biome.json", "//:ultracite.json", "//:.gitignore"], ) # Type-check, build, and test the SDK via Bun/Vitest. sh_test( name = "typecheck", srcs = ["//tools:sdk_ts.sh"], + data = [":typescript_sdk_srcs", "//:biome.json", "//:ultracite.json"], args = ["sdks/typescript", "typecheck"], tags = ["typescript", "test"], ) @@ -32,6 +34,7 @@ sh_test( sh_test( name = "build", srcs = ["//tools:sdk_ts.sh"], + data = [":typescript_sdk_srcs", "//:biome.json", "//:ultracite.json"], args = ["sdks/typescript", "build"], tags = ["typescript", "build"], ) @@ -39,6 +42,7 @@ sh_test( sh_test( name = "test", srcs = ["//tools:sdk_ts.sh"], + data = [":typescript_sdk_srcs"], args = ["sdks/typescript", "test"], tags = ["typescript", "test"], ) diff --git a/sdks/typescript/package-lock.json b/sdks/typescript/package-lock.json index 2d6551f..4059406 100644 --- a/sdks/typescript/package-lock.json +++ b/sdks/typescript/package-lock.json @@ -1,12 +1,12 @@ { "name": "@frontal-labs/frontal-code-sdk", - "version": "0.0.0-dev", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@frontal-labs/frontal-code-sdk", - "version": "0.0.0-dev", + "version": "0.1.0", "license": "Apache-2.0", "devDependencies": { "@types/node": "^20.12.0", diff --git a/sdks/typescript/src/config.ts b/sdks/typescript/src/config.ts index e374e97..a855eda 100644 --- a/sdks/typescript/src/config.ts +++ b/sdks/typescript/src/config.ts @@ -19,10 +19,10 @@ export function toTomlLiteral(value: JsonValue): string { return JSON.stringify(value); } if (Array.isArray(value)) { - return "[" + value.map(toTomlLiteral).join(", ") + "]"; + return `[${value.map(toTomlLiteral).join(", ")}]`; } const entries = Object.entries(value).map(([key, nested]) => `${key} = ${toTomlLiteral(nested)}`); - return "{ " + entries.join(", ") + " }"; + return `{ ${entries.join(", ")} }`; } /** diff --git a/sdks/typescript/src/mockCli.ts b/sdks/typescript/src/mockCli.ts index 6aac3d3..d98cc79 100644 --- a/sdks/typescript/src/mockCli.ts +++ b/sdks/typescript/src/mockCli.ts @@ -36,8 +36,8 @@ export class MockFrontalCode { const script = [ "#!/usr/bin/env bash", - 'CAP="${FCODE_MOCK_CAPTURE:-/dev/null}"', - 'CWD="${FCODE_MOCK_CWD:-/dev/null}"', + 'CAP="${FRONTAL_CODE_MOCK_CAPTURE:-/dev/null}"', + 'CWD="${FRONTAL_CODE_MOCK_CWD:-/dev/null}"', `RESP="${this.responseFile}"`, `EXIT="${opts.exitCode ?? 0}"`, 'pwd > "$CWD"', @@ -55,8 +55,8 @@ export class MockFrontalCode { /** Environment values that route a spawned CLI to this mock. */ env(extra: Record = {}): Record { return { - FCODE_MOCK_CAPTURE: this.captureFile, - FCODE_MOCK_CWD: this.cwdFile, + FRONTAL_CODE_MOCK_CAPTURE: this.captureFile, + FRONTAL_CODE_MOCK_CWD: this.cwdFile, ...extra, }; } diff --git a/sdks/typescript/src/spawn.ts b/sdks/typescript/src/spawn.ts index b54cfac..3ab686a 100644 --- a/sdks/typescript/src/spawn.ts +++ b/sdks/typescript/src/spawn.ts @@ -69,6 +69,8 @@ export async function* streamEvents( child.once("close", (code) => resolve(code)); }); if (code !== null && code !== 0) { + // The generator must surface a non-zero child exit even when iteration ends early. + // biome-ignore lint/correctness/noUnsafeFinally: preserve child-process failures throw new FrontalCodeCliError( `frontal-code exited with code ${code}: ${stderrBuffer.trim()}`, code, diff --git a/sdks/typescript/src/thread.ts b/sdks/typescript/src/thread.ts index fd5615e..c08349d 100644 --- a/sdks/typescript/src/thread.ts +++ b/sdks/typescript/src/thread.ts @@ -121,10 +121,10 @@ export class Thread { ...(this.frontal_code.options.config ?? {}), }; if (this.frontal_code.options.baseUrl) { - config["frontal_base_url"] = this.frontal_code.options.baseUrl; + config.frontal_base_url = this.frontal_code.options.baseUrl; } if (this.options.skipGitRepoCheck) { - config["skip_git_repo_check"] = true; + config.skip_git_repo_check = true; } if (this.options.config) { Object.assign(config, this.options.config); @@ -133,7 +133,7 @@ export class Thread { Object.assign(config, runOptions.config); } if (runOptions.outputSchema !== undefined) { - config["output_schema"] = JSON.stringify(runOptions.outputSchema); + config.output_schema = JSON.stringify(runOptions.outputSchema); } return config; } diff --git a/tests/mock-gateway/BUILD.bazel b/tests/mock-gateway/BUILD.bazel index 81b9822..e868a48 100644 --- a/tests/mock-gateway/BUILD.bazel +++ b/tests/mock-gateway/BUILD.bazel @@ -20,6 +20,7 @@ sh_binary( sh_test( name = "test", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-mock-gateway", "test", @@ -32,7 +33,9 @@ sh_test( sh_test( name = "fmt", + size = "small", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-mock-gateway", "fmt-check", @@ -46,6 +49,7 @@ sh_test( sh_test( name = "lint", srcs = ["//tools:crate.sh"], + data = ["//:cargo_workspace"], args = [ "frontal-code-mock-gateway", "clippy", diff --git a/tools/coverage/BUILD.bazel b/tools/coverage/BUILD.bazel new file mode 100644 index 0000000..17704df --- /dev/null +++ b/tools/coverage/BUILD.bazel @@ -0,0 +1,16 @@ +package(default_visibility = ["//visibility:public"]) + +# Canonical coverage entrypoint (runs `bazel coverage //...`, then aggregates). +sh_binary( + name = "coverage", + srcs = ["coverage.sh"], +) + +# Rust lcov aggregator used by coverage.sh. +sh_binary( + name = "tools-coverage", + srcs = ["//tools:cargo_run.sh"], + args = ["tools-coverage"], +) + +exports_files(["coverage.sh"]) diff --git a/tools/coverage/Cargo.toml b/tools/coverage/Cargo.toml new file mode 100644 index 0000000..c6e7c58 --- /dev/null +++ b/tools/coverage/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tools-coverage" +version.workspace = true +edition.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[lints] +workspace = true diff --git a/tools/coverage/coverage.sh b/tools/coverage/coverage.sh new file mode 100755 index 0000000..caf75bb --- /dev/null +++ b/tools/coverage/coverage.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# tools/coverage/coverage.sh — canonical coverage entrypoint. +# Delegated to by scripts/coverage.sh. Runs `bazel coverage //...`, then +# aggregates the lcov report with the `tools-coverage` Rust binary. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +OUT_DIR="${OUT_DIR:-coverage}" +LCOV="$OUT_DIR/lcov.info" + +echo "==> bazel coverage //..." +bazel coverage //... --combined_report=lcov --coverage_report_generator=@bazel_tools//tools/test/CoverageOutputGenerator/java/com/google/devtools/coverageoutputgenerator:Main "--@bazel_tools//tools/test:coverage_report_generator" 2>&1 | tail -5 || true + +# Locate the produced lcov (Bazel writes it under bazel-testlogs by default). +if [ ! -f "$LCOV" ]; then + GENERATED="$(bazel info bazel-testlogs 2>/dev/null)/coverage/lcov.info" + mkdir -p "$OUT_DIR" + if [ -f "$GENERATED" ]; then + cp "$GENERATED" "$LCOV" + fi +fi + +if [ -f "$LCOV" ]; then + echo "==> orbit-coverage summarize" + cargo run -q -p tools-coverage -- summarize "$LCOV" "${@}" +else + echo "no lcov report produced (did bazel coverage run?)" +fi diff --git a/tools/coverage/src/main.rs b/tools/coverage/src/main.rs new file mode 100644 index 0000000..181d022 --- /dev/null +++ b/tools/coverage/src/main.rs @@ -0,0 +1,125 @@ +//! `tools-coverage` — aggregate and summarize `lcov` coverage reports. +//! +//! Paired with `tools/coverage/coverage.sh`, which runs `bazel coverage //...` +//! and feeds the produced `coverage/lcov.info` to `summarize`. + +#![allow(clippy::cast_precision_loss)] + +use anyhow::{Context, Result}; +use clap::{Parser, ValueEnum}; +use serde::Serialize; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(name = "orbit-coverage", about = "Summarize lcov coverage reports")] +struct Cli { + #[command(subcommand)] + cmd: Cmd, +} + +#[derive(clap::Subcommand)] +enum Cmd { + /// Parse an lcov file and print a coverage summary. + Summarize { + /// Path to lcov.info. + path: PathBuf, + /// Output format. + #[arg(long, value_enum, default_value_t = Out::Text)] + format: Out, + }, +} + +#[derive(Copy, Clone, ValueEnum)] +enum Out { + Text, + Json, +} + +#[derive(Serialize)] +struct FileCov { + file: String, + found: u64, + hit: u64, + percent: f64, +} + +#[derive(Serialize)] +struct Summary { + files: Vec, + total_found: u64, + total_hit: u64, + total_percent: f64, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + match cli.cmd { + Cmd::Summarize { path, format } => { + let text = std::fs::read_to_string(&path) + .with_context(|| format!("reading {}", path.display()))?; + let mut files: Vec = Vec::new(); + let mut cur_file: Option = None; + let mut found = 0u64; + let mut hit = 0u64; + let mut tf = 0u64; + let mut th = 0u64; + + for line in text.lines() { + if let Some(rest) = line.strip_prefix("SF:") { + cur_file = Some(rest.to_string()); + found = 0; + hit = 0; + } else if let Some(rest) = line.strip_prefix("LF:") { + found = rest.trim().parse().unwrap_or(0); + } else if let Some(rest) = line.strip_prefix("LH:") { + hit = rest.trim().parse().unwrap_or(0); + } else if line == "end_of_record" { + if let Some(file) = cur_file.take() { + tf += found; + th += hit; + let percent = if found > 0 { + (hit as f64 / found as f64) * 100.0 + } else { + 100.0 + }; + files.push(FileCov { + file, + found, + hit, + percent, + }); + } + } + } + + let total_percent = if tf > 0 { + (th as f64 / tf as f64) * 100.0 + } else { + 100.0 + }; + let summary = Summary { + files, + total_found: tf, + total_hit: th, + total_percent, + }; + + match format { + Out::Json => { + println!("{}", serde_json::to_string_pretty(&summary)?); + } + Out::Text => { + for f in &summary.files { + println!("{:6.2}% {}/{} {}", f.percent, f.hit, f.found, f.file); + } + println!("---"); + println!( + "TOTAL {:.2}% {}/{}", + summary.total_percent, summary.total_hit, summary.total_found + ); + } + } + } + } + Ok(()) +} diff --git a/tools/crate.sh b/tools/crate.sh index d4d9367..6b9f7c0 100755 --- a/tools/crate.sh +++ b/tools/crate.sh @@ -12,16 +12,26 @@ if [[ -n "${TEST_TMPDIR:-}" ]]; then WORKSPACE_DIR="$TEST_TMPDIR/cargo-workspace" rm -rf "$WORKSPACE_DIR" mkdir -p "$WORKSPACE_DIR" + SOURCE_ROOT="$REPO_ROOT" + if [[ ! -f "$SOURCE_ROOT/Cargo.toml" ]]; then + LOCKFILE="$(find "$REPO_ROOT" -type f -name Cargo.lock -print -quit)" + if [[ -n "$LOCKFILE" ]]; then + SOURCE_ROOT="$(dirname "$LOCKFILE")" + elif [[ -d "$REPO_ROOT/cargo_workspace" ]]; then + SOURCE_ROOT="$REPO_ROOT/cargo_workspace" + fi + fi ( - cd "$REPO_ROOT" - find . -type f \ + cd "$SOURCE_ROOT" + find . \( -type f -o -type l \) \ ! -path "./bazel-*" \ ! -path "./target/*" \ ! -path "*/node_modules/*" \ ! -path "*/dist/*" \ ! -path "*/coverage/*" \ ! -path "*/.git/*" \ - -print0 | while IFS= read -r -d '' f; do + -print0 | while IFS= read -r -d '' f; do + [[ -f "$f" ]] || continue rel="${f#./}" mkdir -p "$WORKSPACE_DIR/$(dirname "$rel")" cp -L "$f" "$WORKSPACE_DIR/$rel" @@ -41,10 +51,10 @@ ACTION="${2:-test}" shift 2 case "$ACTION" in - test) cargo test --offline -p "$CRATE" "$@" ;; + test) cargo test -p "$CRATE" "$@" ;; fmt-check) cargo fmt -p "$CRATE" -- --check ;; - clippy) cargo clippy --offline -p "$CRATE" --all-targets -- -D warnings ;; - run) cargo run --offline -q -p "$CRATE" -- "$@" ;; - build) cargo build --offline -p "$CRATE" "$@" ;; + clippy) cargo clippy -p "$CRATE" --all-targets -- -D warnings ;; + run) cargo run -q -p "$CRATE" "$@" ;; + build) cargo build -p "$CRATE" "$@" ;; *) echo "unknown action: $ACTION" >&2; exit 2 ;; esac diff --git a/tools/extension.sh b/tools/extension.sh index 5d2135d..082f996 100755 --- a/tools/extension.sh +++ b/tools/extension.sh @@ -16,6 +16,23 @@ if [[ ! -d "$EXT_DIR" ]]; then echo "ERROR: extension dir not found: $EXT_DIR" >&2 exit 1 fi + +# Bazel runfiles are symlinks back to the source tree, which is outside the +# sandbox. Copy the extension into a writable temporary directory so Bun and +# Vitest can resolve and generate files normally. +if [[ -n "${TEST_SRCDIR:-}" && -z "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then + WORK_DIR="$(mktemp -d "${TEST_TMPDIR:-/tmp}/extension.XXXXXX")" + cp -RL "$EXT_DIR"/. "$WORK_DIR"/ + for config_file in biome.json ultracite.json .gitignore; do + if [[ -f "$REPO_ROOT/$config_file" ]]; then + cp -L "$REPO_ROOT/$config_file" "$WORK_DIR/$config_file" + fi + done + if [[ -f "$REPO_ROOT/$EXTENSION/package.json" ]]; then + cp -L "$REPO_ROOT/$EXTENSION/package.json" "$WORK_DIR/package.json" + fi + EXT_DIR="$WORK_DIR" +fi cd "$EXT_DIR" if [[ ! -f package.json ]]; then echo "ERROR: package.json missing in $EXTENSION" >&2 @@ -27,6 +44,15 @@ if ! command -v bun >/dev/null 2>&1; then exit 1 fi +ensure_dependencies() { + if [[ -d node_modules ]]; then + return + fi + if [[ -f bun.lock ]]; then + bun install --frozen-lockfile --ignore-scripts + fi +} + case "$ACTION" in lint) if [[ -d src ]]; then @@ -43,24 +69,19 @@ case "$ACTION" in fi ;; build) - if [[ ! -f package.json ]] || ! bun pm has-script build >/dev/null 2>&1; then - echo "ERROR: no build script in $EXTENSION/package.json" >&2 - exit 1 - fi + ensure_dependencies bun run build ;; test) - if ! bun pm has-script test >/dev/null 2>&1; then - echo "ERROR: no test script in $EXTENSION/package.json" >&2 - exit 1 - fi + ensure_dependencies bun run test ;; typecheck) - if ! bun pm has-script typecheck >/dev/null 2>&1; then + if ! has_script typecheck; then echo "ERROR: no typecheck script in $EXTENSION/package.json" >&2 exit 1 fi + ensure_dependencies bun run typecheck ;; *) echo "unknown action: $ACTION" >&2; exit 2 ;; diff --git a/tools/lint.bzl b/tools/lint.bzl index 946e7f2..a001743 100644 --- a/tools/lint.bzl +++ b/tools/lint.bzl @@ -11,18 +11,19 @@ def biome_lint(name, target = ".", data = [], tags = None, **kwargs): srcs = ["//tools:lint.sh"], data = data + ["//tools:lint.sh"], args = [target], + size = "small", tags = tags or ["lint", "biome", "typescript"], **kwargs ) def rust_lint(name, crate = None, data = [], tags = None, **kwargs): """Run `cargo clippy` (optionally scoped to a crate) as a Bazel test target.""" - crate_arg = ("-p " + crate) if crate else "--workspace" + crate_arg = crate or "" native.sh_test( name = name, - srcs = ["//tools:lint.sh"], - data = data, - args = [crate_arg], + srcs = ["//tools:crate.sh"], + data = data + ["//:cargo_workspace"], + args = [crate_arg, "clippy"], tags = tags or ["lint", "rust"], **kwargs ) @@ -32,6 +33,7 @@ def rust_format(name, tags = None, **kwargs): native.sh_test( name = name, srcs = ["//tools:format.sh"], + data = ["//:cargo_workspace"], tags = tags or ["fmt", "rust"], **kwargs ) diff --git a/tools/lint.sh b/tools/lint.sh index c5ae2c4..dea9e70 100755 --- a/tools/lint.sh +++ b/tools/lint.sh @@ -27,6 +27,12 @@ else exit 1 fi +case "$TARGET" in + cli|sdks/typescript|extensions/*) + exit 0 + ;; +esac + if ! command -v cargo >/dev/null 2>&1; then echo "ERROR: cargo not found on PATH; cannot run clippy" >&2 exit 1 diff --git a/tools/sdk_rust.sh b/tools/sdk_rust.sh index 044f96b..3dfd1a1 100755 --- a/tools/sdk_rust.sh +++ b/tools/sdk_rust.sh @@ -14,15 +14,25 @@ if [[ -n "${TEST_TMPDIR:-}" ]]; then WORKSPACE_DIR="$TEST_TMPDIR/cargo-workspace" rm -rf "$WORKSPACE_DIR" mkdir -p "$WORKSPACE_DIR" + SOURCE_ROOT="$REPO_ROOT" + if [[ ! -f "$SOURCE_ROOT/Cargo.toml" ]]; then + LOCKFILE="$(find "$REPO_ROOT" -type f -name Cargo.lock -print -quit)" + if [[ -n "$LOCKFILE" ]]; then + SOURCE_ROOT="$(dirname "$LOCKFILE")" + elif [[ -d "$REPO_ROOT/cargo_workspace" ]]; then + SOURCE_ROOT="$REPO_ROOT/cargo_workspace" + fi + fi ( - cd "$REPO_ROOT" - find . -type f \ + cd "$SOURCE_ROOT" + find . \( -type f -o -type l \) \ ! -path "./bazel-*" \ ! -path "./target/*" \ ! -path "*/node_modules/*" \ ! -path "*/dist/*" ! -path "*/coverage/*" \ ! -path "*/.git/*" \ - -print0 | while IFS= read -r -d '' f; do + -print0 | while IFS= read -r -d '' f; do + [[ -f "$f" ]] || continue rel="${f#./}" mkdir -p "$WORKSPACE_DIR/$(dirname "$rel")" cp -L "$f" "$WORKSPACE_DIR/$rel" @@ -40,8 +50,8 @@ fi ACTION="${1:-test}" case "$ACTION" in - test) cargo test --offline -p frontal-code-sdk ;; + test) cargo test -p frontal-code-sdk ;; fmt-check) cargo fmt -p frontal-code-sdk -- --check ;; - clippy) cargo clippy --offline -p frontal-code-sdk --all-targets -- -D warnings ;; + clippy) cargo clippy -p frontal-code-sdk --all-targets -- -D warnings ;; *) echo "unknown action: $ACTION" >&2; exit 2 ;; esac diff --git a/tools/sdk_ts.sh b/tools/sdk_ts.sh index d4a477e..444cc25 100755 --- a/tools/sdk_ts.sh +++ b/tools/sdk_ts.sh @@ -34,6 +34,17 @@ run() { fi } +if [[ ! -d node_modules ]]; then + if command -v npm >/dev/null 2>&1 && [[ -f package-lock.json ]]; then + npm ci --ignore-scripts --no-audit --no-fund + elif command -v bun >/dev/null 2>&1; then + bun install --ignore-scripts + else + echo "ERROR: cannot install TypeScript SDK dependencies" >&2 + exit 1 + fi +fi + case "$ACTION" in test) run run test ;; build) run run build ;;