clawdforge/clients/ruby/test/test_client.rb

321 lines
9.4 KiB
Ruby

# frozen_string_literal: true
require "minitest/autorun"
require "webmock/minitest"
require "json"
require "tempfile"
require "stringio"
require "clawdforge"
BASE_URL = "http://forge.test:8800"
TOKEN = "cf_test_token_xxxxxxxx"
def build_client(**overrides)
Clawdforge::Client.new(
base_url: BASE_URL,
token: TOKEN,
default_timeout: 60,
**overrides,
)
end
class HealthzTest < Minitest::Test
def test_healthz_returns_parsed_json
stub_request(:get, "#{BASE_URL}/healthz")
.with(headers: { "Authorization" => "Bearer #{TOKEN}" })
.to_return(
status: 200,
headers: { "Content-Type" => "application/json" },
body: JSON.generate(
"ok" => true, "claude_present" => true, "claude_version" => "1.2.3",
),
)
out = build_client.healthz
assert_equal true, out["ok"]
assert_equal "1.2.3", out["claude_version"]
end
end
class RunTest < Minitest::Test
def test_run_success_with_json_result
stub_request(:post, "#{BASE_URL}/run").to_return(
status: 200,
headers: { "Content-Type" => "application/json" },
body: JSON.generate(
"ok" => true,
"result" => { "hello" => "world" },
"duration_ms" => 1234,
"stop_reason" => "end_turn",
),
)
r = build_client.run(prompt: 'Reply with JSON: {"hello": "world"}')
assert_kind_of Clawdforge::RunResult, r
assert_equal true, r.ok
assert_equal({ "hello" => "world" }, r.result)
assert_equal 1234, r.duration_ms
assert_equal "end_turn", r.stop_reason
end
def test_run_success_with_string_result
stub_request(:post, "#{BASE_URL}/run").to_return(
status: 200,
headers: { "Content-Type" => "application/json" },
body: JSON.generate(
"ok" => true, "result" => "plain text reply",
"duration_ms" => 800, "stop_reason" => "end_turn",
),
)
r = build_client.run(prompt: "hello")
assert_equal "plain text reply", r.result
end
def test_run_sends_full_body_and_auth
captured = {}
stub_request(:post, "#{BASE_URL}/run").with do |req|
captured[:auth] = req.headers["Authorization"]
captured[:body] = JSON.parse(req.body)
true
end.to_return(
status: 200,
headers: { "Content-Type" => "application/json" },
body: JSON.generate("ok" => true, "result" => {}, "duration_ms" => 1, "stop_reason" => "end_turn"),
)
build_client.run(
prompt: "hi", model: "opus", system: "be terse",
files: ["ff_abc"], timeout_secs: 42,
)
assert_equal "Bearer #{TOKEN}", captured[:auth]
assert_equal(
{
"prompt" => "hi", "model" => "opus", "system" => "be terse",
"files" => ["ff_abc"], "timeout_secs" => 42,
},
captured[:body],
)
end
def test_run_uses_default_model_and_omits_optional_fields
captured = {}
stub_request(:post, "#{BASE_URL}/run").with do |req|
captured[:body] = JSON.parse(req.body)
true
end.to_return(
status: 200,
headers: { "Content-Type" => "application/json" },
body: JSON.generate("ok" => true, "result" => "x", "duration_ms" => 1, "stop_reason" => "end_turn"),
)
build_client.run(prompt: "hi")
assert_equal "sonnet", captured[:body]["model"]
refute captured[:body].key?("system")
refute captured[:body].key?("files")
refute captured[:body].key?("timeout_secs")
end
def test_run_502_raises_api_error_with_body
stub_request(:post, "#{BASE_URL}/run").to_return(
status: 502,
headers: { "Content-Type" => "application/json" },
body: JSON.generate(
"ok" => false, "error" => "subprocess timed out",
"stderr" => "...", "duration_ms" => 60_000, "stop_reason" => "timeout",
),
)
err = assert_raises(Clawdforge::APIError) do
build_client.run(prompt: "hi", timeout_secs: 60)
end
assert_equal 502, err.status
assert_kind_of Hash, err.body
assert_equal "timeout", err.body["stop_reason"]
assert_match(/subprocess timed out/, err.message)
end
def test_run_401_raises_auth_error
stub_request(:post, "#{BASE_URL}/run").to_return(
status: 401,
headers: { "Content-Type" => "application/json" },
body: JSON.generate("detail" => "missing bearer"),
)
err = assert_raises(Clawdforge::AuthError) { build_client.run(prompt: "hi") }
assert_equal 401, err.status
assert_kind_of Clawdforge::APIError, err
assert_kind_of Clawdforge::Error, err
end
def test_run_transport_error_when_unreachable
stub_request(:post, "#{BASE_URL}/run").to_raise(SocketError.new("getaddrinfo: nope"))
assert_raises(Clawdforge::TransportError) { build_client.run(prompt: "hi") }
end
def test_run_empty_prompt_rejected_locally
assert_raises(ArgumentError) { build_client.run(prompt: "") }
end
end
class FilesTest < Minitest::Test
def test_upload_file_from_path
stub_request(:post, "#{BASE_URL}/files")
.with(headers: { "Content-Type" => %r{\Amultipart/form-data; boundary=} })
.to_return(
status: 200,
headers: { "Content-Type" => "application/json" },
body: JSON.generate("file_token" => "ff_abc123", "ttl_secs" => 3600, "size" => 11),
)
Tempfile.create(["upload", ".txt"]) do |tf|
tf.binmode
tf.write("hello world")
tf.flush
ft = build_client.upload_file(tf.path, ttl_secs: 3600)
assert_kind_of Clawdforge::FileToken, ft
assert_equal "ff_abc123", ft.file_token
assert_equal 3600, ft.ttl_secs
assert_equal 11, ft.size
end
end
def test_upload_file_sends_multipart_with_ttl_and_file_parts
captured = {}
stub_request(:post, "#{BASE_URL}/files").with do |req|
captured[:content_type] = req.headers["Content-Type"]
captured[:body] = req.body
true
end.to_return(
status: 200,
headers: { "Content-Type" => "application/json" },
body: JSON.generate("file_token" => "ff_xyz", "ttl_secs" => 60, "size" => 5),
)
Tempfile.create(["snippet", ".bin"]) do |tf|
tf.binmode
tf.write("hello")
tf.flush
build_client.upload_file(tf.path, ttl_secs: 60, filename: "snippet.txt")
end
assert_match(%r{\Amultipart/form-data; boundary=}, captured[:content_type])
assert_match(/name="ttl_secs"/, captured[:body])
assert_match(/name="file"; filename="snippet\.txt"/, captured[:body])
assert_match(/hello/, captured[:body])
end
def test_upload_file_400_raises_api_error
stub_request(:post, "#{BASE_URL}/files").to_return(
status: 400,
headers: { "Content-Type" => "application/json" },
body: JSON.generate("detail" => "ttl_secs out of range (60..86400)"),
)
Tempfile.create(["bad", ".bin"]) do |tf|
tf.binmode
tf.write("x")
tf.flush
err = assert_raises(Clawdforge::APIError) do
build_client.upload_file(tf.path, ttl_secs: 10)
end
assert_equal 400, err.status
end
end
end
class AdminTokensTest < Minitest::Test
def test_create_token
stub_request(:post, "#{BASE_URL}/admin/tokens").to_return(
status: 200,
headers: { "Content-Type" => "application/json" },
body: JSON.generate(
"name" => "cauldron",
"token" => "cf_brandnew_xxx",
"ip_cidrs" => ["172.24.0.0/16"],
),
)
t = build_client.create_token("cauldron", ip_cidrs: ["172.24.0.0/16"])
assert_kind_of Clawdforge::AppToken, t
assert_equal "cauldron", t.name
assert_equal "cf_brandnew_xxx", t.token
assert_equal ["172.24.0.0/16"], t.ip_cidrs
end
def test_list_tokens_normalizes_csv_ip_cidrs
stub_request(:get, "#{BASE_URL}/admin/tokens").to_return(
status: 200,
headers: { "Content-Type" => "application/json" },
body: JSON.generate(
"tokens" => [
{
"name" => "cauldron", "ip_cidrs" => "172.24.0.0/16",
"created_at" => 100, "last_used" => 200, "enabled" => 1,
},
{
"name" => "petalparse", "ip_cidrs" => "",
"created_at" => 50, "last_used" => nil, "enabled" => 0,
},
],
),
)
toks = build_client.list_tokens
assert_equal 2, toks.length
assert_equal "cauldron", toks[0].name
assert_equal ["172.24.0.0/16"], toks[0].ip_cidrs
assert_equal true, toks[0].enabled
assert_nil toks[0].token
assert_equal [], toks[1].ip_cidrs
assert_equal false, toks[1].enabled
end
def test_revoke_token_ok
stub_request(:delete, "#{BASE_URL}/admin/tokens/cauldron").to_return(
status: 200,
headers: { "Content-Type" => "application/json" },
body: JSON.generate("ok" => true),
)
assert_equal true, build_client.revoke_token("cauldron")
end
def test_revoke_token_404
stub_request(:delete, "#{BASE_URL}/admin/tokens/nosuch").to_return(
status: 404,
headers: { "Content-Type" => "application/json" },
body: JSON.generate("detail" => "no such token"),
)
err = assert_raises(Clawdforge::APIError) { build_client.revoke_token("nosuch") }
assert_equal 404, err.status
end
end
class ClientConstructionTest < Minitest::Test
def test_requires_base_url
assert_raises(ArgumentError) { Clawdforge::Client.new(base_url: "", token: "cf_x") }
end
def test_requires_token
assert_raises(ArgumentError) { Clawdforge::Client.new(base_url: "http://x", token: "") }
end
def test_strips_trailing_slash
c = Clawdforge::Client.new(base_url: "http://x:8800/", token: "cf_x")
assert_equal "http://x:8800", c.base_url
end
end