diff options
| author | Adam Malczewski <[email protected]> | 2026-04-29 21:40:16 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-04-29 21:40:16 +0900 |
| commit | 5c9b8f5142198bdf230d500b5101322a22235670 (patch) | |
| tree | fa80010970db89f4ee03f0d493bed26d47eb0ce6 /spec/dispatch | |
| parent | 1ec2afaa21b8c3ef336982e80259b9bb79e3fb32 (diff) | |
| download | dispatch-adapter-copilot-5c9b8f5142198bdf230d500b5101322a22235670.tar.gz dispatch-adapter-copilot-5c9b8f5142198bdf230d500b5101322a22235670.zip | |
update to match new interface
Diffstat (limited to 'spec/dispatch')
| -rw-r--r-- | spec/dispatch/adapter/copilot_rate_limiting_spec.rb | 12 | ||||
| -rw-r--r-- | spec/dispatch/adapter/copilot_spec.rb | 481 | ||||
| -rw-r--r-- | spec/dispatch/adapter/rate_limiter_spec.rb | 10 |
3 files changed, 361 insertions, 142 deletions
diff --git a/spec/dispatch/adapter/copilot_rate_limiting_spec.rb b/spec/dispatch/adapter/copilot_rate_limiting_spec.rb index 6a7ef2b..a51ed2b 100644 --- a/spec/dispatch/adapter/copilot_rate_limiting_spec.rb +++ b/spec/dispatch/adapter/copilot_rate_limiting_spec.rb @@ -12,12 +12,12 @@ RSpec.describe Dispatch::Adapter::Copilot, "rate limiting" do let(:chat_response_body) do JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }) end - let(:messages) { [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] } + let(:messages) { [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] } before do stub_request(:get, "https://api.github.com/copilot_internal/v2/token") @@ -181,8 +181,8 @@ RSpec.describe Dispatch::Adapter::Copilot, "rate limiting" do describe "#chat streaming with rate limiting" do it "calls wait! before a streaming request" do sse_body = [ - "data: #{JSON.generate({ "choices" => [ { "delta" => { "content" => "hi" }, "index" => 0 } ] })}\n\n", - "data: #{JSON.generate({ "choices" => [ { "delta" => {}, "index" => 0, "finish_reason" => "stop" } ], + "data: #{JSON.generate({ "choices" => [{ "delta" => { "content" => "hi" }, "index" => 0 }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ "delta" => {}, "index" => 0, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } })}\n\n", "data: [DONE]\n\n" ].join @@ -211,12 +211,12 @@ RSpec.describe Dispatch::Adapter::Copilot, "rate limiting" do .to_return( status: 200, body: JSON.generate({ - "data" => [ { + "data" => [{ "id" => "gpt-4.1", "name" => "GPT 4.1", "model_picker_enabled" => true, "capabilities" => { "type" => "chat", "supports" => {} } - } ] + }] }), headers: { "Content-Type" => "application/json" } ) diff --git a/spec/dispatch/adapter/copilot_spec.rb b/spec/dispatch/adapter/copilot_spec.rb index f3ccb37..15bdcb0 100644 --- a/spec/dispatch/adapter/copilot_spec.rb +++ b/spec/dispatch/adapter/copilot_spec.rb @@ -6,6 +6,32 @@ RSpec.describe Dispatch::Adapter::Copilot do let(:copilot_token) { "cop_test_token_abc" } let(:github_token) { "gho_test_github_token" } + # --------------------------------------------------------------------------- + # SAFETY: this MUST be the first test in the file. If WebMock is not + # globally blocking real network access, every other test in this gem could + # potentially hit the real GitHub Copilot API and consume premium-request + # quota / leak credentials. If this test fails, STOP and fix spec_helper + # before running any other spec. + # --------------------------------------------------------------------------- + describe "!! network safety !!" do + it "globally blocks real outbound HTTP via WebMock" do + expect(WebMock.net_connect_allowed?).to be(false) + end + + it "raises when an unstubbed request is attempted" do + expect do + Net::HTTP.get(URI("https://api.githubcopilot.com/never-should-fire")) + end.to raise_error(WebMock::NetConnectNotAllowedError) + end + + it "forbids localhost as well (no accidental dev-server contact)" do + # The spec_helper passes allow_localhost: false. Verify it. + expect do + Net::HTTP.get(URI("http://127.0.0.1:1/should-not-fire")) + end.to raise_error(WebMock::NetConnectNotAllowedError) + end + end + let(:adapter) do described_class.new( model: "gpt-4.1", @@ -37,7 +63,7 @@ RSpec.describe Dispatch::Adapter::Copilot do describe "VERSION" do it "is accessible" do -expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") + expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.4.0") end end @@ -73,11 +99,11 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") body: JSON.generate({ "id" => "chatcmpl-123", "model" => "gpt-4.1", - "choices" => [ { + "choices" => [{ "index" => 0, "message" => { "role" => "assistant", "content" => "Hello there!" }, "finish_reason" => "stop" - } ], + }], "usage" => { "prompt_tokens" => 10, "completion_tokens" => 5 } }), headers: { "Content-Type" => "application/json" } @@ -85,7 +111,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") end it "returns a Response with content" do - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] response = adapter.chat(messages) expect(response).to be_a(Dispatch::Adapter::Response) @@ -106,22 +132,22 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") body: JSON.generate({ "id" => "chatcmpl-456", "model" => "gpt-4.1", - "choices" => [ { + "choices" => [{ "index" => 0, "message" => { "role" => "assistant", "content" => nil, - "tool_calls" => [ { + "tool_calls" => [{ "id" => "call_abc", "type" => "function", "function" => { "name" => "get_weather", "arguments" => '{"city":"New York"}' } - } ] + }] }, "finish_reason" => "tool_calls" - } ], + }], "usage" => { "prompt_tokens" => 15, "completion_tokens" => 10 } }), headers: { "Content-Type" => "application/json" } @@ -129,7 +155,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") end it "returns a Response with tool_calls as ToolUseBlock array" do - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "What's the weather?") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "What's the weather?")] response = adapter.chat(messages) expect(response.content).to be_nil @@ -150,7 +176,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { + "choices" => [{ "index" => 0, "message" => { "role" => "assistant", @@ -169,7 +195,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") ] }, "finish_reason" => "tool_calls" - } ], + }], "usage" => { "prompt_tokens" => 20, "completion_tokens" => 15 } }), headers: { "Content-Type" => "application/json" } @@ -177,7 +203,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") end it "returns multiple ToolUseBlocks" do - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "weather and time?") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "weather and time?")] response = adapter.chat(messages) expect(response.tool_calls.size).to eq(2) @@ -197,22 +223,22 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") body: JSON.generate({ "id" => "chatcmpl-789", "model" => "gpt-4.1", - "choices" => [ { + "choices" => [{ "index" => 0, "message" => { "role" => "assistant", "content" => "Let me check that for you.", - "tool_calls" => [ { + "tool_calls" => [{ "id" => "call_def", "type" => "function", "function" => { "name" => "search", "arguments" => '{"query":"Ruby gems"}' } - } ] + }] }, "finish_reason" => "tool_calls" - } ], + }], "usage" => { "prompt_tokens" => 20, "completion_tokens" => 15 } }), headers: { "Content-Type" => "application/json" } @@ -220,7 +246,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") end it "returns both content and tool_calls" do - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Search for Ruby gems") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Search for Ruby gems")] response = adapter.chat(messages) expect(response.content).to eq("Let me check that for you.") @@ -239,13 +265,13 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "OK" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "OK" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] adapter.chat(messages, system: "You are helpful.") expect(stub).to have_been_requested @@ -262,13 +288,13 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "short" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "short" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] adapter.chat(messages, max_tokens: 100) expect(stub).to have_been_requested @@ -283,13 +309,13 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] adapter.chat(messages) expect(stub).to have_been_requested @@ -307,26 +333,26 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") .with do |req| body = JSON.parse(req.body) - body["tools"] == [ { + body["tools"] == [{ "type" => "function", "function" => { "name" => "get_weather", "description" => "Get weather for a city", "parameters" => { "type" => "object", "properties" => { "city" => { "type" => "string" } } } } - } ] + }] end .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "weather?") ] - adapter.chat(messages, tools: [ tool ]) + messages = [Dispatch::Adapter::Message.new(role: "user", content: "weather?")] + adapter.chat(messages, tools: [tool]) expect(stub).to have_been_requested end @@ -341,26 +367,26 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") .with do |req| body = JSON.parse(req.body) - body["tools"] == [ { + body["tools"] == [{ "type" => "function", "function" => { "name" => "get_weather", "description" => "Get weather for a city", "parameters" => { "type" => "object", "properties" => { "city" => { "type" => "string" } } } } - } ] + }] end .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "weather?") ] - adapter.chat(messages, tools: [ tool_hash ]) + messages = [Dispatch::Adapter::Message.new(role: "user", content: "weather?")] + adapter.chat(messages, tools: [tool_hash]) expect(stub).to have_been_requested end @@ -375,26 +401,26 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") .with do |req| body = JSON.parse(req.body) - body["tools"] == [ { + body["tools"] == [{ "type" => "function", "function" => { "name" => "get_weather", "description" => "Get weather for a city", "parameters" => { "type" => "object", "properties" => { "city" => { "type" => "string" } } } } - } ] + }] end .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "weather?") ] - adapter.chat(messages, tools: [ tool_hash ]) + messages = [Dispatch::Adapter::Message.new(role: "user", content: "weather?")] + adapter.chat(messages, tools: [tool_hash]) expect(stub).to have_been_requested end @@ -421,14 +447,14 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "both?") ] - adapter.chat(messages, tools: [ tool_struct, tool_hash ]) + messages = [Dispatch::Adapter::Message.new(role: "user", content: "both?")] + adapter.chat(messages, tools: [tool_struct, tool_hash]) expect(stub).to have_been_requested end @@ -442,13 +468,13 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] adapter.chat(messages) expect(stub).to have_been_requested @@ -466,8 +492,8 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") messages = [ Dispatch::Adapter::Message.new(role: "user", content: "What's the weather?"), - Dispatch::Adapter::Message.new(role: "assistant", content: [ tool_use ]), - Dispatch::Adapter::Message.new(role: "user", content: [ tool_result ]) + Dispatch::Adapter::Message.new(role: "assistant", content: [tool_use]), + Dispatch::Adapter::Message.new(role: "user", content: [tool_result]) ] stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") @@ -488,8 +514,8 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "It's 72F and sunny in NYC!" }, - "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "It's 72F and sunny in NYC!" }, + "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 20, "completion_tokens" => 10 } }), headers: { "Content-Type" => "application/json" } @@ -503,7 +529,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") context "with ImageBlock" do it "raises NotImplementedError" do image = Dispatch::Adapter::ImageBlock.new(source: "base64data", media_type: "image/png") - messages = [ Dispatch::Adapter::Message.new(role: "user", content: [ image ]) ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: [image])] expect { adapter.chat(messages) }.to raise_error(NotImplementedError, /ImageBlock/) end @@ -515,7 +541,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") Dispatch::Adapter::TextBlock.new(text: "First paragraph."), Dispatch::Adapter::TextBlock.new(text: "Second paragraph.") ] - messages = [ Dispatch::Adapter::Message.new(role: "user", content: text_blocks) ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: text_blocks)] stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") .with do |req| @@ -526,7 +552,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } @@ -552,8 +578,8 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") messages = [ Dispatch::Adapter::Message.new(role: "user", content: "search"), - Dispatch::Adapter::Message.new(role: "assistant", content: [ tool_use ]), - Dispatch::Adapter::Message.new(role: "user", content: [ tool_result ]) + Dispatch::Adapter::Message.new(role: "assistant", content: [tool_use]), + Dispatch::Adapter::Message.new(role: "user", content: [tool_result]) ] stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") @@ -566,7 +592,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 10, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } @@ -588,8 +614,8 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") messages = [ Dispatch::Adapter::Message.new(role: "user", content: "do it"), - Dispatch::Adapter::Message.new(role: "assistant", content: [ tool_use ]), - Dispatch::Adapter::Message.new(role: "user", content: [ tool_result ]) + Dispatch::Adapter::Message.new(role: "assistant", content: [tool_use]), + Dispatch::Adapter::Message.new(role: "user", content: [tool_result]) ] stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") @@ -602,8 +628,8 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "I see the error" }, - "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "I see the error" }, + "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 10, "completion_tokens" => 3 } }), headers: { "Content-Type" => "application/json" } @@ -620,16 +646,16 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { + "choices" => [{ "message" => { "content" => "truncated output..." }, "finish_reason" => "length" - } ], + }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 100 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Write a long essay") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Write a long essay")] response = adapter.chat(messages) expect(response.stop_reason).to eq(:max_tokens) @@ -646,7 +672,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") messages = [ Dispatch::Adapter::Message.new(role: "user", content: "lookup 42"), - Dispatch::Adapter::Message.new(role: "assistant", content: [ text, tool_use ]) + Dispatch::Adapter::Message.new(role: "assistant", content: [text, tool_use]) ] stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") @@ -662,7 +688,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 10, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } @@ -689,7 +715,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } @@ -710,14 +736,14 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "thought deeply" }, - "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "thought deeply" }, + "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 3 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Think hard") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Think hard")] adapter.chat(messages, thinking: "high") expect(stub).to have_been_requested @@ -739,13 +765,13 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] thinking_adapter.chat(messages) expect(stub).to have_been_requested @@ -767,13 +793,13 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] thinking_adapter.chat(messages, thinking: "low") expect(stub).to have_been_requested @@ -788,14 +814,17 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] - adapter.chat(messages) + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] + # Explicitly disable thinking for this single call. The default adapter + # is constructed with thinking: "high", so we must pass thinking: nil + # per-call to suppress reasoning_effort. + adapter.chat(messages, thinking: nil) expect(stub).to have_been_requested end @@ -807,7 +836,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") end it "raises ArgumentError for invalid per-call thinking level" do - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] expect do adapter.chat(messages, thinking: "extreme") end.to raise_error(ArgumentError, /Invalid thinking level/) @@ -829,13 +858,13 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] thinking_adapter.chat(messages, thinking: nil) expect(stub).to have_been_requested @@ -843,12 +872,202 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") end end + describe "X-Initiator header (premium request billing)" do + # GitHub Copilot only bills requests sent with `X-Initiator: user` as + # premium requests. Continuations inside a tool/agent loop must be sent + # with `X-Initiator: agent` to avoid being billed. + # + # This adapter uses the "savings" strategy: the very first send for a + # conversation (only system + user) is `user`; every subsequent send + # (containing any assistant or tool message) is `agent`. + + let(:ok_response) do + { + status: 200, + body: JSON.generate({ + "choices" => [{ "message" => { "content" => "ok" }, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } + }), + headers: { "Content-Type" => "application/json" } + } + end + + it "sends X-Initiator: user for the first request (user message only)" do + stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") + .with(headers: { "X-Initiator" => "user" }) + .to_return(**ok_response) + + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] + adapter.chat(messages) + + expect(stub).to have_been_requested + end + + it "sends X-Initiator: user when only a system + user message are present" do + stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") + .with(headers: { "X-Initiator" => "user" }) + .to_return(**ok_response) + + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] + adapter.chat(messages, system: "You are a helpful assistant.") + + expect(stub).to have_been_requested + end + + it "sends X-Initiator: agent when sending a tool result back to the model" do + # The classic agent-loop continuation: prior assistant tool_use + + # current tool_result. This MUST NOT be billed as premium. + tool_use = Dispatch::Adapter::ToolUseBlock.new( + id: "call_1", name: "get_weather", arguments: { "city" => "NYC" } + ) + tool_result = Dispatch::Adapter::ToolResultBlock.new( + tool_use_id: "call_1", content: "72F and sunny" + ) + + messages = [ + Dispatch::Adapter::Message.new(role: "user", content: "What's the weather?"), + Dispatch::Adapter::Message.new(role: "assistant", content: [tool_use]), + Dispatch::Adapter::Message.new(role: "user", content: [tool_result]) + ] + + stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") + .with(headers: { "X-Initiator" => "agent" }) + .to_return(**ok_response) + + adapter.chat(messages) + + expect(stub).to have_been_requested + end + + it "sends X-Initiator: agent when an assistant text turn is present (multi-turn)" do + # Savings semantics: any prior assistant turn in history flips this to + # `agent`, even if the latest message is a fresh user prompt. This is + # intentional and more aggressive than VS Code. + messages = [ + Dispatch::Adapter::Message.new(role: "user", content: "Hi"), + Dispatch::Adapter::Message.new(role: "assistant", content: "Hello!"), + Dispatch::Adapter::Message.new(role: "user", content: "And again?") + ] + + stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") + .with(headers: { "X-Initiator" => "agent" }) + .to_return(**ok_response) + + adapter.chat(messages) + + expect(stub).to have_been_requested + end + + it "sends codecompanion-equivalent fingerprint headers alongside X-Initiator" do + # We mimic codecompanion.nvim's wire profile exactly: + # Copilot-Integration-Id: vscode-chat + # Editor-Version: Neovim/<version> + # X-Initiator: user|agent + # We DO NOT send Openai-Intent (codecompanion does not). + stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") + .with(headers: { + "Copilot-Integration-Id" => "vscode-chat", + "Editor-Version" => "Neovim/0.10.4", + "X-Initiator" => "user" + }) + .to_return(**ok_response) + + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] + adapter.chat(messages) + + expect(stub).to have_been_requested + end + + it "does not send the Openai-Intent header (codecompanion parity)" do + captured_headers = nil + stub_request(:post, "https://api.githubcopilot.com/chat/completions") + .with do |req| + captured_headers = req.headers + true + end + .to_return(**ok_response) + + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] + adapter.chat(messages) + + expect(captured_headers.keys.map(&:downcase)).not_to include("openai-intent") + end + + it "allows overriding Editor-Version via constructor" do + custom_adapter = described_class.new( + model: "gpt-4.1", + github_token: github_token, + max_tokens: 4096, + min_request_interval: 0, + editor_version: "Neovim/0.12.1" + ) + + stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") + .with(headers: { "Editor-Version" => "Neovim/0.12.1" }) + .to_return(**ok_response) + + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] + custom_adapter.chat(messages) + + expect(stub).to have_been_requested + end + + context "in streaming mode" do + let(:sse_ok) do + { + status: 200, + body: [ + "data: #{JSON.generate({ "choices" => [{ "delta" => { "content" => "ok" }, "index" => 0 }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ "delta" => {}, "index" => 0, "finish_reason" => "stop" }], + "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } })}\n\n", + "data: [DONE]\n\n" + ].join, + headers: { "Content-Type" => "text/event-stream" } + } + end + + it "sends X-Initiator: user for the initial streaming request" do + stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") + .with(headers: { "X-Initiator" => "user" }) + .to_return(**sse_ok) + + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] + adapter.chat(messages, stream: true) { |_d| } + + expect(stub).to have_been_requested + end + + it "sends X-Initiator: agent for streaming tool-result continuations" do + tool_use = Dispatch::Adapter::ToolUseBlock.new( + id: "call_1", name: "search", arguments: { "q" => "x" } + ) + tool_result = Dispatch::Adapter::ToolResultBlock.new( + tool_use_id: "call_1", content: "result" + ) + + messages = [ + Dispatch::Adapter::Message.new(role: "user", content: "search x"), + Dispatch::Adapter::Message.new(role: "assistant", content: [tool_use]), + Dispatch::Adapter::Message.new(role: "user", content: [tool_result]) + ] + + stub = stub_request(:post, "https://api.githubcopilot.com/chat/completions") + .with(headers: { "X-Initiator" => "agent" }) + .to_return(**sse_ok) + + adapter.chat(messages, stream: true) { |_d| } + + expect(stub).to have_been_requested + end + end + end + describe "#chat with streaming" do it "yields StreamDelta objects and returns Response" do sse_body = [ - "data: #{JSON.generate({ "choices" => [ { "delta" => { "content" => "Hello" }, "index" => 0 } ] })}\n\n", - "data: #{JSON.generate({ "choices" => [ { "delta" => { "content" => " world" }, "index" => 0 } ] })}\n\n", - "data: #{JSON.generate({ "choices" => [ { "delta" => {}, "index" => 0, "finish_reason" => "stop" } ], + "data: #{JSON.generate({ "choices" => [{ "delta" => { "content" => "Hello" }, "index" => 0 }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ "delta" => { "content" => " world" }, "index" => 0 }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ "delta" => {}, "index" => 0, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 2 } })}\n\n", "data: [DONE]\n\n" ].join @@ -861,7 +1080,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") headers: { "Content-Type" => "text/event-stream" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] deltas = [] response = adapter.chat(messages, stream: true) { |delta| deltas << delta } @@ -878,20 +1097,20 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") it "yields tool_use_start and tool_use_delta for tool call streams" do sse_body = [ - "data: #{JSON.generate({ "choices" => [ { - "delta" => { "tool_calls" => [ { "index" => 0, "id" => "call_1", "type" => "function", - "function" => { "name" => "search", "arguments" => "" } } ] }, "index" => 0 - } ] })}\n\n", - "data: #{JSON.generate({ "choices" => [ { - "delta" => { "tool_calls" => [ { "index" => 0, - "function" => { "arguments" => "{\"q\":" } } ] }, "index" => 0 - } ] })}\n\n", - "data: #{JSON.generate({ "choices" => [ { - "delta" => { "tool_calls" => [ { "index" => 0, - "function" => { "arguments" => "\"test\"}" } } ] }, "index" => 0 - } ] })}\n\n", - "data: #{JSON.generate({ "choices" => [ { "delta" => {}, "index" => 0, - "finish_reason" => "tool_calls" } ] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ + "delta" => { "tool_calls" => [{ "index" => 0, "id" => "call_1", "type" => "function", + "function" => { "name" => "search", "arguments" => "" } }] }, "index" => 0 + }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ + "delta" => { "tool_calls" => [{ "index" => 0, + "function" => { "arguments" => "{\"q\":" } }] }, "index" => 0 + }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ + "delta" => { "tool_calls" => [{ "index" => 0, + "function" => { "arguments" => "\"test\"}" } }] }, "index" => 0 + }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ "delta" => {}, "index" => 0, + "finish_reason" => "tool_calls" }] })}\n\n", "data: [DONE]\n\n" ].join @@ -902,7 +1121,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") headers: { "Content-Type" => "text/event-stream" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "search") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "search")] deltas = [] response = adapter.chat(messages, stream: true) { |delta| deltas << delta } @@ -923,8 +1142,8 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") it "captures usage from streaming response" do sse_body = [ - "data: #{JSON.generate({ "choices" => [ { "delta" => { "content" => "hi" }, "index" => 0 } ] })}\n\n", - "data: #{JSON.generate({ "choices" => [ { "delta" => {}, "index" => 0, "finish_reason" => "stop" } ], + "data: #{JSON.generate({ "choices" => [{ "delta" => { "content" => "hi" }, "index" => 0 }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ "delta" => {}, "index" => 0, "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 42, "completion_tokens" => 7 } })}\n\n", "data: [DONE]\n\n" ].join @@ -936,7 +1155,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") headers: { "Content-Type" => "text/event-stream" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] response = adapter.chat(messages, stream: true) { |_delta| nil } expect(response.usage.input_tokens).to eq(42) @@ -945,31 +1164,31 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") it "handles multiple parallel tool calls in a stream" do sse_body = [ - "data: #{JSON.generate({ "choices" => [ { - "delta" => { "tool_calls" => [ { "index" => 0, "id" => "call_a", "type" => "function", - "function" => { "name" => "tool_a", "arguments" => "" } } ] }, "index" => 0 - } ] })}\n\n", - "data: #{JSON.generate({ "choices" => [ { - "delta" => { "tool_calls" => [ { "index" => 1, "id" => "call_b", "type" => "function", - "function" => { "name" => "tool_b", "arguments" => "" } } ] }, "index" => 0 - } ] })}\n\n", - "data: #{JSON.generate({ "choices" => [ { - "delta" => { "tool_calls" => [ { "index" => 0, - "function" => { "arguments" => "{\"x\":1}" } } ] }, "index" => 0 - } ] })}\n\n", - "data: #{JSON.generate({ "choices" => [ { - "delta" => { "tool_calls" => [ { "index" => 1, - "function" => { "arguments" => "{\"y\":2}" } } ] }, "index" => 0 - } ] })}\n\n", - "data: #{JSON.generate({ "choices" => [ { "delta" => {}, "index" => 0, - "finish_reason" => "tool_calls" } ] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ + "delta" => { "tool_calls" => [{ "index" => 0, "id" => "call_a", "type" => "function", + "function" => { "name" => "tool_a", "arguments" => "" } }] }, "index" => 0 + }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ + "delta" => { "tool_calls" => [{ "index" => 1, "id" => "call_b", "type" => "function", + "function" => { "name" => "tool_b", "arguments" => "" } }] }, "index" => 0 + }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ + "delta" => { "tool_calls" => [{ "index" => 0, + "function" => { "arguments" => "{\"x\":1}" } }] }, "index" => 0 + }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ + "delta" => { "tool_calls" => [{ "index" => 1, + "function" => { "arguments" => "{\"y\":2}" } }] }, "index" => 0 + }] })}\n\n", + "data: #{JSON.generate({ "choices" => [{ "delta" => {}, "index" => 0, + "finish_reason" => "tool_calls" }] })}\n\n", "data: [DONE]\n\n" ].join stub_request(:post, "https://api.githubcopilot.com/chat/completions") .to_return(status: 200, body: sse_body, headers: { "Content-Type" => "text/event-stream" }) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "do both") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "do both")] deltas = [] response = adapter.chat(messages, stream: true) { |d| deltas << d } @@ -999,14 +1218,14 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") .to_return( status: 200, body: JSON.generate({ - "choices" => [ { "message" => { "content" => "ok" }, - "finish_reason" => "stop" } ], + "choices" => [{ "message" => { "content" => "ok" }, + "finish_reason" => "stop" }], "usage" => { "prompt_tokens" => 5, "completion_tokens" => 1 } }), headers: { "Content-Type" => "application/json" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] adapter.chat(messages) adapter.chat(messages) @@ -1025,7 +1244,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") ) fresh_adapter = described_class.new(model: "gpt-4.1", github_token: "bad_token", max_tokens: 4096) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] expect { fresh_adapter.chat(messages) }.to raise_error(Dispatch::Adapter::AuthenticationError) end @@ -1277,7 +1496,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") stub_request(:post, "https://api.githubcopilot.com/chat/completions") .to_return(status: 401, body: JSON.generate({ "error" => { "message" => "Unauthorized" } })) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::AuthenticationError) { |e| expect(e.status_code).to eq(401) expect(e.provider).to eq("GitHub Copilot") @@ -1288,7 +1507,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") stub_request(:post, "https://api.githubcopilot.com/chat/completions") .to_return(status: 403, body: JSON.generate({ "error" => { "message" => "Forbidden" } })) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::AuthenticationError) end @@ -1300,7 +1519,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") headers: { "Retry-After" => "30" } ) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::RateLimitError) { |e| expect(e.status_code).to eq(429) expect(e.retry_after).to eq(30) @@ -1311,7 +1530,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") stub_request(:post, "https://api.githubcopilot.com/chat/completions") .to_return(status: 400, body: JSON.generate({ "error" => { "message" => "Bad request" } })) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::RequestError) { |e| expect(e.status_code).to eq(400) } @@ -1321,7 +1540,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") stub_request(:post, "https://api.githubcopilot.com/chat/completions") .to_return(status: 422, body: JSON.generate({ "error" => { "message" => "Unprocessable" } })) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::RequestError) end @@ -1329,7 +1548,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") stub_request(:post, "https://api.githubcopilot.com/chat/completions") .to_return(status: 500, body: JSON.generate({ "error" => { "message" => "Internal error" } })) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::ServerError) { |e| expect(e.status_code).to eq(500) } @@ -1339,7 +1558,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") stub_request(:post, "https://api.githubcopilot.com/chat/completions") .to_return(status: 502, body: "Bad Gateway") - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::ServerError) end @@ -1347,7 +1566,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") stub_request(:post, "https://api.githubcopilot.com/chat/completions") .to_return(status: 503, body: "Service Unavailable") - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::ServerError) end @@ -1355,7 +1574,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") stub_request(:post, "https://api.githubcopilot.com/chat/completions") .to_raise(Errno::ECONNREFUSED.new("Connection refused")) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::ConnectionError) { |e| expect(e.provider).to eq("GitHub Copilot") } @@ -1365,7 +1584,7 @@ expect(Dispatch::Adapter::Copilot::VERSION).to eq("0.3.0") stub_request(:post, "https://api.githubcopilot.com/chat/completions") .to_raise(Net::OpenTimeout.new("execution expired")) - messages = [ Dispatch::Adapter::Message.new(role: "user", content: "Hi") ] + messages = [Dispatch::Adapter::Message.new(role: "user", content: "Hi")] expect { adapter.chat(messages) }.to raise_error(Dispatch::Adapter::ConnectionError) end end diff --git a/spec/dispatch/adapter/rate_limiter_spec.rb b/spec/dispatch/adapter/rate_limiter_spec.rb index 1e4e501..5fcf92f 100644 --- a/spec/dispatch/adapter/rate_limiter_spec.rb +++ b/spec/dispatch/adapter/rate_limiter_spec.rb @@ -240,7 +240,7 @@ RSpec.describe Dispatch::Adapter::RateLimiter do now = Time.now.to_f state = { "last_request_at" => now, - "request_log" => [ now - 2.0, now - 1.0, now ] + "request_log" => [now - 2.0, now - 1.0, now] } File.write(rate_limit_path, JSON.generate(state)) @@ -252,7 +252,7 @@ RSpec.describe Dispatch::Adapter::RateLimiter do now = Time.now.to_f state = { "last_request_at" => now - 5.0, - "request_log" => [ now - 15.0, now - 12.0, now - 5.0 ] + "request_log" => [now - 15.0, now - 12.0, now - 5.0] } File.write(rate_limit_path, JSON.generate(state)) @@ -264,7 +264,7 @@ RSpec.describe Dispatch::Adapter::RateLimiter do now = Time.now.to_f state = { "last_request_at" => now - 5.0, - "request_log" => [ now - 20.0, now - 15.0, now - 5.0 ] + "request_log" => [now - 20.0, now - 15.0, now - 5.0] } File.write(rate_limit_path, JSON.generate(state)) @@ -299,7 +299,7 @@ RSpec.describe Dispatch::Adapter::RateLimiter do now = Time.now.to_f state = { "last_request_at" => now - 2.0, # cooldown elapsed - "request_log" => [ now - 3.0, now - 2.5, now - 2.0 ] # window full + "request_log" => [now - 3.0, now - 2.5, now - 2.0] # window full } File.write(rate_limit_path, JSON.generate(state)) @@ -380,7 +380,7 @@ RSpec.describe Dispatch::Adapter::RateLimiter do it "reads state written by another process" do # Simulate another process having made a request just now now = Time.now.to_f - state = { "last_request_at" => now, "request_log" => [ now ] } + state = { "last_request_at" => now, "request_log" => [now] } FileUtils.mkdir_p(File.dirname(rate_limit_path)) File.write(rate_limit_path, JSON.generate(state)) |
