Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ jobs:
deps-${{ runner.os }}-otp${{ steps.beam.outputs.otp-version }}-elixir${{ steps.beam.outputs.elixir-version }}-

- name: Restore build cache
uses: actions/cache@v4
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: _build
key: build-${{ runner.os }}-otp${{ steps.beam.outputs.otp-version }}-elixir${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('mix.lock') }}
restore-keys: |
build-${{ runner.os }}-otp${{ steps.beam.outputs.otp-version }}-elixir${{ steps.beam.outputs.elixir-version }}-

- name: Restore PLT cache
uses: actions/cache@v4
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: priv/plts
key: plt-${{ runner.os }}-otp${{ steps.beam.outputs.otp-version }}-elixir${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('mix.lock') }}
Expand All @@ -51,11 +51,23 @@ jobs:
- name: Install dependencies
run: mix deps.get

- name: Build PLT
run: mkdir -p priv/plts && mix dialyzer --plt

- name: Check for outdated dependencies
run: mix hex.outdated --all
run: mix hex.outdated
Comment thread
thanos marked this conversation as resolved.

- name: Audit dependencies for known vulnerabilities
run: mix hex.audit

- name: Run full verification
run: mix verify
- name: Check formatting
run: mix format --check-formatted

- name: Compile with warnings as errors
run: mix compile --warnings-as-errors

- name: Run Credo
run: mix credo --strict

- name: Check documentation
run: mix docs 2>&1 | tee docs_output.txt && ! grep -i "warning" docs_output.txt
Comment thread
thanos marked this conversation as resolved.
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@

All notable changes to this project will be documented in this file.

## [0.3.2] - 2026-04-19

### Added
- Comprehensive unit tests for Client module: 71 tests covering GenServer callbacks, provider parsing, auth parsing, file validation, search, OpenAPI conversion, and monitoring endpoints
- Comprehensive unit tests for GraphQL transport: 23 new tests covering GenServer callbacks (init, handle_call, handle_info, terminate), state transitions, and type specifications
- Comprehensive unit tests for HTTP transport: SSE streaming helpers, URL parameter substitution, discovery response parsing, tool response parsing, header building, and schema parsing
- Unit tests for GraphQL Connection module: 34 tests covering struct definition, public API, GenServer callbacks, state transitions, and terminate handling
- Unit tests for TCP/UDP Pool: 37 tests covering connection management, cleanup, and lifecycle
- Unit tests for MCP Pool: 29 tests covering connection tracking, cleanup, and provider management
- Extended MCP Connection tests: 25 additional tests covering GenServer callbacks and retry logic
- CI workflows with pinned GitHub Action commit SHAs for supply-chain security
- Behaviour module for WebRTC connection (ExUtcp.Transports.WebRTC.ConnectionBehaviour)
- Testable module for WebRTC with Mox support

### Changed
- Test coverage increased from 52.0% to 55.5% across the codebase
- GraphQL Connection: fixed bug where `handle_call(:get_last_used)` and `handle_call(:update_last_used)` referenced `last_used_at` instead of the struct field `last_used`
- WebRTC provider type spec: removed `url` and `auth` keys that were never present in actual provider maps created by `Providers.new_webrtc_provider/1`
- GitHub Actions: pinned `actions/checkout` to v4.2.2, `erlef/setup-beam` to v1.18.0, and `actions/cache` to v4.2.3 using full commit SHAs
- Test suite now excludes integration tests by default via `ExUnit.start(exclude: [:integration])`
- Removed all emoji from documentation files, replaced with plain text equivalents

### Fixed
- GraphQL Connection: `handle_call(:get_last_used)` and `handle_call(:update_last_used)` used wrong field name (`last_used_at` vs `last_used`)
- WebRTC provider type spec: `url` and `auth` keys marked as required but never created in actual provider maps
- Documentation inconsistencies in TEST_COVERAGE_REPORT.md: corrected test counts for sse_mock_test (19->22) and testable_validation_test (22->30)
- Documentation inconsistency in ZERO_WARNINGS_ACHIEVED.md: Sobelow findings listed as "0" but should be "6 (all mitigated)"
- Documentation inconsistency in WARNINGS_FIXED.md: remaining warnings count said "3" but actual count is "2"
- Documentation error in COMPARISON_STUDY.md: Python comment syntax (`//`) used in Python example block instead of `#`

## [0.3.1] - 2025-10-05

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Key characteristics:
* Test configuration with integration test exclusion by default
* Advanced Search: Multiple algorithms with fuzzy matching and semantic search
* Monitoring and Metrics: Telemetry, PromEx, health checks, and performance monitoring
* Comprehensive test suite with 497+ tests
* Comprehensive test suite with 1473+ tests

## Installation

Expand All @@ -43,7 +43,7 @@ Add `ex_utcp` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ex_utcp, "~> 0.2.0"}
{:ex_utcp, "~> 0.3.2"}
]
end
```
Expand Down
31 changes: 31 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,35 @@

## Release Notes - ExUtcp v0.3.2

### Overview
ExUtcp v0.3.2 is a quality and testing release focused on improving test coverage, fixing bugs, hardening CI security, and correcting documentation. Test coverage increased from 52.0% to 55.5%, and multiple bugs were identified and fixed during testing.

### Changes from v0.3.1

#### Added
- 71 unit tests for the Client module covering GenServer callbacks, provider/auth parsing, file validation, search, OpenAPI conversion, and monitoring.
- 23 new tests for the GraphQL transport covering GenServer callbacks and state management.
- Extended HTTP transport tests with SSE streaming, discovery parsing, header building, and schema parsing.
- 34 tests for GraphQL Connection module: struct definition, public API, GenServer callbacks, and terminate handling.
- 37 tests for TCP/UDP Pool and 29 tests for MCP Pool covering connection management and cleanup.
- 25 additional MCP Connection tests covering callbacks and retry logic.
- WebRTC ConnectionBehaviour module and Testable module with Mox support.
- CI workflows with pinned GitHub Action commit SHAs for supply-chain security.

#### Changed
- Test coverage increased from 52.0% to 55.5%.
- Integration tests now excluded by default (`ExUnit.start(exclude: [:integration])`).
- All emoji removed from documentation, replaced with plain text equivalents.
- 1473 total tests (0 failures, 133 excluded, 81 skipped).

#### Fixed
- GraphQL Connection: `handle_call(:get_last_used)` and `handle_call(:update_last_used)` referenced non-existent field `last_used_at` instead of `last_used`.
- WebRTC provider type spec: removed `url` and `auth` keys that were required by the type but never created in actual provider maps.
- GitHub Actions: pinned all actions to full commit SHAs (`actions/checkout@11bd7190`, `erlef/setup-beam@a6e26b22`, `actions/cache@5a3ec84e`).
Comment thread
thanos marked this conversation as resolved.
- Documentation: corrected test counts, Sobelow findings count, warnings count, and Python comment syntax.

---

## Release Notes - ExUtcp v0.3.1

### Overview
Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule ExUtcp.MixProject do
def project do
[
app: :ex_utcp,
version: "0.3.1",
version: "0.3.2",
elixir: "~> 1.15",
start_permanent: Mix.env() == :prod,
elixirc_paths: elixirc_paths(Mix.env()),
Expand Down Expand Up @@ -137,6 +137,7 @@ defmodule ExUtcp.MixProject do
defp verify(_) do
steps = [
# ["precommit", :dev],
{"hex.outdated", :dev},
{"compile --warnings-as-errors", :dev},
{"format --check-formatted", :dev},
{"credo --strict", :dev},
Expand Down
32 changes: 24 additions & 8 deletions test/ex_utcp/config_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ defmodule ExUtcp.ConfigTest do
test "returns variable from loader module" do
# Use the module atom directly - Config calls loader.get(key)
loader = __MODULE__.TestLoader
config = Config.new(load_variables_from: [loader])
Config.new(load_variables_from: [loader])
# This won't work because TestLoader.get/2 needs a struct
# Skip this test for now - loaders need proper implementation
end
Expand All @@ -100,14 +100,18 @@ defmodule ExUtcp.ConfigTest do

test "returns error when variable not found" do
config = Config.new(variables: %{})
assert {:error, %{variable_name: "NONEXISTENT"}} = Config.get_variable(config, "NONEXISTENT")

assert {:error, %{variable_name: "NONEXISTENT"}} =
Config.get_variable(config, "NONEXISTENT")
end
end

describe "substitute_variables/2" do
test "substitutes ${VAR} pattern" do
config = Config.new(variables: %{"HOST" => "example.com"})
assert Config.substitute_variables(config, "https://${HOST}/api") == "https://example.com/api"

assert Config.substitute_variables(config, "https://${HOST}/api") ==
"https://example.com/api"
end

test "substitutes $VAR pattern" do
Expand All @@ -117,12 +121,16 @@ defmodule ExUtcp.ConfigTest do

test "substitutes multiple variables" do
config = Config.new(variables: %{"HOST" => "example.com", "PORT" => "8080"})
assert Config.substitute_variables(config, "https://${HOST}:${PORT}/api") == "https://example.com:8080/api"

assert Config.substitute_variables(config, "https://${HOST}:${PORT}/api") ==
"https://example.com:8080/api"
end

test "leaves unsubstituted variables intact" do
config = Config.new(variables: %{})
assert Config.substitute_variables(config, "https://${UNKNOWN}/api") == "https://${UNKNOWN}/api"

assert Config.substitute_variables(config, "https://${UNKNOWN}/api") ==
"https://${UNKNOWN}/api"
end

test "substitutes in list values" do
Expand All @@ -132,7 +140,10 @@ defmodule ExUtcp.ConfigTest do

test "substitutes in map values" do
config = Config.new(variables: %{"KEY" => "substituted"})
assert Config.substitute_variables(config, %{"field" => "${KEY}"}) == %{"field" => "substituted"}

assert Config.substitute_variables(config, %{"field" => "${KEY}"}) == %{
"field" => "substituted"
}
end

test "returns non-string, non-map, non-list values unchanged" do
Expand All @@ -144,7 +155,9 @@ defmodule ExUtcp.ConfigTest do

test "handles mixed content in string" do
config = Config.new(variables: %{"VAR" => "replaced"})
assert Config.substitute_variables(config, "prefix_${VAR}_suffix") == "prefix_replaced_suffix"

assert Config.substitute_variables(config, "prefix_${VAR}_suffix") ==
"prefix_replaced_suffix"
end

test "handles $VAR pattern - matches word chars only" do
Expand All @@ -155,7 +168,10 @@ defmodule ExUtcp.ConfigTest do

test "nested map substitution" do
config = Config.new(variables: %{"HOST" => "example.com"})
result = Config.substitute_variables(config, %{"url" => "https://${HOST}", "static" => "value"})

result =
Config.substitute_variables(config, %{"url" => "https://${HOST}", "static" => "value"})

assert result == %{"url" => "https://example.com", "static" => "value"}
end
end
Expand Down
10 changes: 7 additions & 3 deletions test/ex_utcp/transports/graphql/connection_unit_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ defmodule ExUtcp.Transports.Graphql.ConnectionUnitTest do

test "sets default max_retries when not provided" do
provider = %{name: "test", url: "http://example.com/graphql"}
opts = []
# opts = []

# Can't test init directly without HTTP, but struct creation works
state = %Connection{
Expand Down Expand Up @@ -190,7 +190,9 @@ defmodule ExUtcp.Transports.Graphql.ConnectionUnitTest do

from = {self(), :test_ref}

result = Connection.handle_call({:subscription, "subscription { test }", %{}, []}, from, state)
result =
Connection.handle_call({:subscription, "subscription { test }", %{}, []}, from, state)

assert match?({:reply, {:error, _}, ^state}, result)
end

Expand All @@ -209,7 +211,9 @@ defmodule ExUtcp.Transports.Graphql.ConnectionUnitTest do

from = {self(), :test_ref}

result = Connection.handle_call({:subscription, "subscription { test }", %{}, []}, from, state)
result =
Connection.handle_call({:subscription, "subscription { test }", %{}, []}, from, state)

assert match?({:reply, {:error, _}, _}, result)
end
end
Expand Down
24 changes: 18 additions & 6 deletions test/ex_utcp/transports/graphql_unit_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ defmodule ExUtcp.Transports.GraphqlUnitTest do
@tag :skip
test "initializes with default opts" do
result = Graphql.init([])
assert match?({:ok, state}, result)
assert match?({:ok, _state}, result)
{:ok, state} = result
assert %Graphql{} = state
assert state.connection_timeout == 30_000
Expand All @@ -454,7 +454,7 @@ defmodule ExUtcp.Transports.GraphqlUnitTest do
@tag :skip
test "initializes with custom opts" do
result = Graphql.init(connection_timeout: 60_000, max_retries: 5, retry_delay: 2000)
assert match?({:ok, state}, result)
assert match?({:ok, _state}, result)
{:ok, state} = result
assert state.connection_timeout == 60_000
assert state.max_retries == 5
Expand Down Expand Up @@ -528,7 +528,9 @@ defmodule ExUtcp.Transports.GraphqlUnitTest do
from = {self(), :test_ref}
provider = %{type: :graphql, name: "test", url: "http://invalid:9999/graphql"}

result = Graphql.handle_call({:mutation, provider, "mutation { test }", %{}, []}, from, state)
result =
Graphql.handle_call({:mutation, provider, "mutation { test }", %{}, []}, from, state)

assert match?({:reply, {:error, _}, ^state}, result)
end
end
Expand All @@ -540,7 +542,13 @@ defmodule ExUtcp.Transports.GraphqlUnitTest do
from = {self(), :test_ref}
provider = %{type: :graphql, name: "test", url: "http://invalid:9999/graphql"}

result = Graphql.handle_call({:subscription, provider, "subscription { test }", %{}, []}, from, state)
result =
Graphql.handle_call(
{:subscription, provider, "subscription { test }", %{}, []},
from,
state
)

assert match?({:reply, {:error, _}, ^state}, result)
end
end
Expand Down Expand Up @@ -593,15 +601,19 @@ defmodule ExUtcp.Transports.GraphqlUnitTest do

describe "build_graphql_subscription/2" do
test "creates subscription with simple tool name" do
{type, subscription_string, variables} = build_graphql_subscription("updates", %{"filter" => "active"})
{type, subscription_string, variables} =
build_graphql_subscription("updates", %{"filter" => "active"})

assert type == :subscription
assert subscription_string =~ "subscription updates"
assert subscription_string =~ "updates(input:"
assert variables == %{"input" => %{"filter" => "active"}}
end

test "replaces dots in subscription name" do
{type, subscription_string, _variables} = build_graphql_subscription("stream.v2.updates", %{})
{type, subscription_string, _variables} =
build_graphql_subscription("stream.v2.updates", %{})

assert type == :subscription
assert subscription_string =~ "subscription stream_v2_updates"
end
Expand Down
Loading
Loading