test: align spec suite with baseline, and fix three client bugs it found - #537
Merged
Conversation
Bring the Ruby spec suite in line with the scope and strategy used by seamapi/javascript-http: specs are organized by SDK concern, exercise the SDK against fake-seam-connect, and assert against seeded records rather than hand-written stubs. Fixture: - Add FakeSeamConnect, which starts the fake on an unused port, polls /health until it is ready, reads the seed, and stops the server after the example. Examples opt in with `:fake`. - Start the fake directly from node_modules/.bin rather than through `npm run start`, so the spawned pid is the server itself and no process group juggling is needed to stop it. - Replace spec/support/helpers.rb, whose stub_seam_request helper encouraged stubbing over exercising the SDK. Scope: - Drop spec/clients, which covered generated route methods rather than SDK behavior. The JavaScript SDK has no equivalent specs and reaches its routes only as a means of exercising the client. - Drop the overlapping request specs and the mocked action attempt resource spec, folding what they covered into http_error_spec and wait_for_action_attempt_spec against the fake. - Exclude generated routes and resources from coverage, so the report stops rewarding specs for generated code. - Fix the misspelled wait_for_action_attepmt_spec filename. Coverage: - Convert the api key, personal access token, env, http error, wait for action attempt, faraday options, and multi workspace specs to the fake. - Add serialization, headers, retry, and defaults specs. - Cover invalid input validation messages, paginator argument checks, and the personal access token option checks. WebMock is kept for the two cases the fake cannot serve, asserting the request the SDK sends and counting retry attempts, which is the role nock plays in the JavaScript SDK. Two specs are marked pending, both bugs the previous stubs concealed: - The client option cannot be used on its own. initialize parses the auth options before it considers the client, so it still demands an api_key or personal_access_token. - wait_for_action_attempt set to a hash on the client never waits. SingleWorkspace wraps its defaults in a DeepHashAccessor, so the value no longer satisfies the is_a?(Hash) check in decide_and_wait. The old spec passed only because its stub returned a successful action attempt and never had to wait. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DzapiU8A9NMdyoTybL9xB
Move the DeepHashAccessor spec out of lib/seam so all specs live in one place, and drop the extra Rakefile pattern that existed to reach it. Rename the BaseResource hash spec after its subject, which is BaseResource rather than DeepHashAccessor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DzapiU8A9NMdyoTybL9xB
SingleWorkspace#initialize parsed the auth options before it considered the client, so passing only a client raised SeamInvalidOptionsError. Supplying an api_key alongside one made the option pointless, since the auth headers were built and then discarded with the client that would have carried them. Parse the auth options only when a client has to be built, so a client from another instance can be reused the way SeamHttp.fromClient allows in the JavaScript SDK. Drops the pending marker from the spec that covered this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DzapiU8A9NMdyoTybL9xB
SingleWorkspace wraps its defaults in a DeepHashAccessor, so a hash
passed as wait_for_action_attempt reached decide_and_wait as an accessor
rather than a Hash. The is_a?(Hash) branch was skipped and the client
returned a pending action attempt without waiting, so
Seam.new(wait_for_action_attempt: {timeout: 5}) never waited. The same
option passed on a method call arrives as a plain Hash and did work.
Accept either form in decide_and_wait, and add DeepHashAccessor#to_h so
the wrapped value can be read back as the hash it was built from.
Drops the pending marker from the spec that covered this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DzapiU8A9NMdyoTybL9xB
Seam.from_api_key and Seam.from_personal_access_token defaulted wait_for_action_attempt to false while Seam.new defaulted it to true, so which constructor was used silently decided whether unlock_door returned a resolved action attempt or a pending one. The README already documents the option as enabled by default, and the other Seam SDKs enable it by default, so the false defaults in the from_* constructors were an oversight rather than an intended difference. Default the option to true in Seam, Seam::Http, and Seam::Http::SingleWorkspace so every constructor agrees. Callers who want the old behavior can still pass wait_for_action_attempt: false. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DzapiU8A9NMdyoTybL9xB
razor-x
added a commit
that referenced
this pull request
Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the Ruby spec suite in line with the scope and strategy used by seamapi/javascript-http, then fixes the three SDK bugs that work uncovered.
Five commits, split so each fix is reviewable on its own:
test: align spec suite with the JavaScript SDK baselinetest: keep every spec under spec/fix: allow the client option to be used on its ownhttp_single_workspace.rbfix: wait when wait_for_action_attempt is a hash on the clienthelpers/action_attempt.rb,deep_hash_accessor.rbfix: wait for action attempts by default in the from_* constructorsseam.rb,http.rb,http_single_workspace.rbEach fix commit is green on its own, and carries the spec that stops being skipped.
The baseline this follows
The JavaScript SDK has one fixture,
getTestServer, that gives every test a freshly seeded fake. Its test files are named after SDK concerns — auth, env, headers, http errors, pagination, retry, serialization, waiting on action attempts — and they reach for routes likedevices.getonly as a means of exercising the client. There are no per-route tests, because routes are generated.nockappears only where the fake cannot help.The Ruby suite had the shape inverted:
spec/clientsheld ten files of per-route WebMock stubs, while the fake was used by just two specs.Fixture
FakeSeamConnect, which starts the fake on an unused port, polls/healthuntil ready, reads the seed, and stops the server after the example. Examples opt in with:fakeand getendpoint,seed, andseam.node_modules/.binrather than throughnpm run start, so the spawned pid is the server itself and no process-group juggling is needed to stop it.spec/support/helpers.rb, whosestub_seam_requesthelper made stubbing the path of least resistance.Scope
spec/clients— ten files asserting that generated route methods return the resource class they were generated to return. The JavaScript SDK has no equivalent.http_error_specandwait_for_action_attempt_specagainst the fake. The action attempt spec had stubbed:geton/action_attempts/getwhile the SDK POSTs to it.wait_for_action_attepmt_specfilename, andspec/clients/action_attempts_spec.rbwhich declaredRSpec.describe Seam::Clients::AccessCodes.DeepHashAccessorspec out oflib/seamso every spec lives underspec/.Coverage
serialization_spec,headers_spec,retry_spec, anddefaults_spec.WebMock is kept for the two cases the fake cannot serve — asserting the request the SDK sends, and counting retry attempts, since a simulated outage stays in place for every request. That is the role
nockplays in the JavaScript SDK.Fix 1: the
client:option could not be used on its ownSingleWorkspace#initializeparsed the auth options before it consideredclient, so passing only a client raisedSeamInvalidOptionsError. Supplying anapi_keyalongside one made the option pointless, since the auth headers were built and then thrown away with the client that would have carried them.The auth options are now parsed only when a client has to be built, so a client from another instance can be reused the way
SeamHttp.fromClientallows in the JavaScript SDK.Fix 2:
wait_for_action_attemptas a hash on the client never waitedSingleWorkspacewraps its defaults in aDeepHashAccessor, which deep-wraps nested hashes. So a hash passed aswait_for_action_attemptreacheddecide_and_waitas an accessor rather than aHash, theis_a?(Hash)branch was skipped, and the client returned a pending action attempt without waiting.Seam.new(wait_for_action_attempt: {timeout: 5})never waited. The same option passed on a method call arrives as a plainHashand did work — that asymmetry is now covered both ways.decide_and_waitaccepts either form, andDeepHashAccessor#to_hlets the wrapped value be read back as the hash it was built from.This one is a good argument for the rest of the PR: the old spec passed only because its stub returned an already-successful action attempt, so the code path that waits was never exercised. Running against the fake exposed it immediately.
Fix 3:
from_api_keyandfrom_personal_access_tokendid not wait by defaultSeam.newdefaultedwait_for_action_attempttotruewhileSeam.from_api_keyandSeam.from_personal_access_tokendefaulted it tofalse, so which constructor you picked silently decided whetherunlock_doorhanded back a resolved action attempt or a pending one.The README already documents the option as enabled by default, and the other Seam SDKs enable it by default, so this was an oversight rather than an intended difference. All three layers —
Seam,Seam::Http,Seam::Http::SingleWorkspace— now default it totrue. Callers who want the old behavior can still passwait_for_action_attempt: false, anddefaults_speccovers that.Note this is a behavior change for anyone using the
from_*constructors: those calls will now block until the action attempt resolves, and can raiseActionAttemptFailedErrororActionAttemptTimeoutErrorwhere they previously returned a pending attempt. That is the documented behavior and matches every other constructor, but it is the one change here that existing callers could notice.Verification
rake lintis clean.