Skip to content
23 changes: 20 additions & 3 deletions .github/workflows/code-qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,34 @@ jobs:
restore-keys: |
${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-
${{ runner.os }}-turbo-
- name: Run unit tests with coverage
- name: Run test coverage with split core integration lane
run: pnpm test:coverage
Comment thread
roomote[bot] marked this conversation as resolved.
Outdated
- name: Upload coverage to Codecov
# Coverage is uploaded in three separate steps so each LCOV gets the
# correct flag set. Codecov double-counts overlapping lines when a
# single upload carries multiple flags whose paths overlap, so the
# two core lanes (which both cover packages/core/src/**) must be
# uploaded individually with their own lane flag.
# See https://docs.codecov.com/docs/flags
- name: Upload non-core coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: >-
src/coverage/lcov.info,
webview-ui/coverage/lcov.info,
packages/core/coverage/lcov.info,
packages/cloud/coverage/lcov.info,
packages/telemetry/coverage/lcov.info,
apps/cli/coverage/lcov.info
flags: ${{ matrix.codecov-flag }}
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload core unit coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: packages/core/coverage/unit/lcov.info
flags: ${{ matrix.codecov-flag }},core-unit
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload core integration coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: packages/core/coverage/integration/lcov.info
flags: ${{ matrix.codecov-flag }},core-integration
token: ${{ secrets.CODECOV_TOKEN }}
11 changes: 11 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ coverage:
target: 80% # new lines must be 80% covered
threshold: 0%

flag_management:
individual_flags:
- name: core-unit
paths:
- packages/core/src/
carryforward: true
- name: core-integration
paths:
- packages/core/src/
carryforward: true

comment:
layout: "diff, flags"
behavior: default
6 changes: 5 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
"lint": "eslint src --ext=ts --max-warnings=0",
"check-types": "tsc --noEmit",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:unit": "vitest run --config vitest.unit.config.ts",
"test:integration": "vitest run --config vitest.integration.config.ts",
"test:coverage": "pnpm test:coverage:unit && pnpm test:coverage:integration",
"test:coverage:unit": "vitest run --config vitest.unit.config.ts --coverage",
"test:coverage:integration": "vitest run --config vitest.integration.config.ts --coverage",
"clean": "rimraf .turbo"
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// pnpm --filter @roo-code/core test src/custom-tools/__tests__/custom-tool-registry.integration.spec.ts

import path from "path"
import { fileURLToPath } from "url"

import { CustomToolRegistry } from "../custom-tool-registry.js"

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const TEST_FIXTURES_DIR = path.join(__dirname, "fixtures")
const TEST_FIXTURES_OVERRIDE_DIR = path.join(__dirname, "fixtures-override")

describe.sequential("CustomToolRegistry integration", () => {
let registry: CustomToolRegistry

beforeEach(() => {
registry = new CustomToolRegistry()
})

describe("loadFromDirectory", () => {
it("should load tools from TypeScript files", async () => {
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)

expect(result.loaded).toContain("simple")
expect(registry.has("simple")).toBe(true)
}, 300_000)

it("should handle named exports", async () => {
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)

expect(result.loaded).toContain("multi_toolA")
expect(result.loaded).toContain("multi_toolB")
}, 30_000)

it("should report validation failures", async () => {
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)

const invalidFailure = result.failed.find((failure) => failure.file === "invalid.ts")
expect(invalidFailure).toBeDefined()
expect(invalidFailure?.error).toContain("Invalid tool definition")
}, 30_000)

it("should return empty results for non-existent directory", async () => {
const result = await registry.loadFromDirectory("/nonexistent/path")

expect(result.loaded).toHaveLength(0)
expect(result.failed).toHaveLength(0)
})

it("should skip non-tool exports silently", async () => {
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)

expect(result.loaded).toContain("mixed_validTool")
expect(result.loaded).not.toContain("mixed_someString")
expect(result.loaded).not.toContain("mixed_someNumber")
expect(result.loaded).not.toContain("mixed_someObject")
}, 30_000)

it("should support args as alias for parameters", async () => {
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)

expect(result.loaded).toContain("legacy")

const tool = registry.get("legacy")
expect(tool?.parameters).toBeDefined()
}, 30_000)
})

describe("clearCache", () => {
it("should clear the TypeScript compilation cache", async () => {
await registry.loadFromDirectory(TEST_FIXTURES_DIR)
registry.clearCache()

registry.clear()
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)

expect(result.loaded).toContain("cached")
}, 300_000)
})

describe("loadFromDirectories", () => {
it("should load tools from multiple directories", async () => {
const result = await registry.loadFromDirectories([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])

expect(result.loaded).toContain("simple")
expect(result.loaded).toContain("unique_override")
expect(result.loaded).toContain("multi_toolA")
}, 60_000)

it("should allow later directories to override earlier ones", async () => {
await registry.loadFromDirectories([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])

const simpleTool = registry.get("simple")
expect(simpleTool).toBeDefined()
expect(simpleTool?.description).toBe("Simple tool - OVERRIDDEN")
}, 60_000)

it("should preserve order: first directory loaded first, second overrides", async () => {
await registry.loadFromDirectories([TEST_FIXTURES_OVERRIDE_DIR, TEST_FIXTURES_DIR])

const simpleTool = registry.get("simple")
expect(simpleTool).toBeDefined()
expect(simpleTool?.description).toBe("Simple tool")
}, 60_000)

it("should handle non-existent directories in the array", async () => {
const result = await registry.loadFromDirectories([
"/nonexistent/path",
TEST_FIXTURES_DIR,
"/another/nonexistent",
])

expect(result.loaded).toContain("simple")
expect(result.failed).toHaveLength(1)
}, 60_000)

it("should handle empty array", async () => {
const result = await registry.loadFromDirectories([])

expect(result.loaded).toHaveLength(0)
expect(result.failed).toHaveLength(0)
})

it("should combine results from all directories", async () => {
const result = await registry.loadFromDirectories([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])

const simpleCount = result.loaded.filter((name) => name === "simple").length
expect(simpleCount).toBe(2)
}, 60_000)
})

describe("loadFromDirectoriesIfStale", () => {
it("should load tools from multiple directories when stale", async () => {
const result = await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])

expect(result.loaded).toContain("simple")
expect(result.loaded).toContain("unique_override")
}, 60_000)

it("should not reload if directories are not stale", async () => {
await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR])
registry.clear()

const result = await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR])

expect(result.loaded).toEqual([])
}, 30_000)

it("should handle mixed stale and non-stale directories", async () => {
await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR])

const result = await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])

expect(result.loaded).toContain("simple")
expect(result.loaded).toContain("unique_override")
}, 60_000)
})
})
Loading
Loading