feat(bubble-ups): wrap bubbling a recording up and down - #839
Conversation
Adds CreateBubbleUp and DeleteBubbleUp — the per-recording write side of
Bubble Up (the BC5 successor to "save"), which had no SDK coverage:
GetBubbleUps only models the /my/readings list.
- CreateBubbleUp: POST /recordings/{id}/bubble_up.json (204), optional `at`
timing field ("now" immediate; a keyword or ISO8601 date schedules).
- DeleteBubbleUp: DELETE /recordings/{id}/bubble_up.json (204).
Both answer head :no_content from Recordings::BubbleUpsController and are
naturally idempotent (set membership), mirroring the DeleteBookmark shape.
New BubbleUps tag/service across all six SDKs (Go wrapper + accessor;
TS/Ruby/Python client accessors; Swift/Kotlin generated). Conformance path
+ idempotency cases dispatched in all six runners; Go/TS/Ruby/Python unit
tests covering create (at present/absent) and delete.
The per-recording status GET stays an API gap: its show.json.jbuilder lives
outside app/views/api/, unrenderable on the API host (the GetRecording trap).
Registered in spec/api-gaps/recording-bubble-up-write.md (partial-coverage).
The two flat routes are waived in bc3-route-allowlist.yml with routes.rb +
controller evidence, since bc3's doc/api documents neither spelling yet.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
🟡 Changes recommended
Conformance payload assertions, asynchronous Python coverage, and several documentation counts remain incomplete.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds Smithy-first Bubble Up create/delete support across all six SDKs, including generated services, client accessors, retry metadata, and conformance coverage.
Changes:
- Adds idempotent
CreateBubbleUpandDeleteBubbleUpoperations. - Regenerates six SDK surfaces and wires service accessors.
- Adds tests, route waivers, gap tracking, and documentation updates.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or rungh pr ready --undo.
Click "Ready for review" or rungh pr readyto reengage.
File summaries
| File | Description |
|---|---|
typescript/tests/services/bubble-ups.test.ts |
Tests TypeScript operations. |
typescript/src/index.ts |
Exports Bubble Ups APIs. |
typescript/src/generated/services/index.ts |
Exports generated service. |
typescript/src/generated/services/bubble-ups.ts |
Implements generated service. |
typescript/src/generated/schema.d.ts |
Adds generated operation types. |
typescript/src/generated/path-mapping.ts |
Maps Bubble Up routes. |
typescript/src/generated/openapi-stripped.json |
Adds stripped OpenAPI operations. |
typescript/src/generated/metadata.ts |
Adds retry metadata. |
typescript/src/client.ts |
Wires the service accessor. |
swift/Sources/Basecamp/Generated/Services/BubbleUpsService.swift |
Implements Swift service. |
swift/Sources/Basecamp/Generated/Models/CreateBubbleUpRequest.swift |
Adds Swift request model. |
swift/Sources/Basecamp/Generated/Metadata.swift |
Adds Swift retry metadata. |
swift/Sources/Basecamp/Generated/AccountClient+Services.swift |
Adds Swift accessor. |
swift/README.md |
Updates service counts. |
spec/zero-skip-roster.yml |
Records Ruby retry waivers. |
spec/overlays/tags.smithy |
Assigns the BubbleUps tag. |
spec/bc3-route-allowlist.yml |
Waives undocumented BC3 routes. |
spec/basecamp.smithy |
Defines both operations. |
spec/api-gaps/recording-bubble-up-write.md |
Tracks partial API coverage. |
spec/api-gaps/README.md |
Registers the API gap. |
SPEC.md |
Updates inventories and counts. |
SECURITY.md |
Updates retry classification. |
scripts/check-idempotency-parity |
Updates expected idempotency totals. |
ruby/test/basecamp/services/bubble_ups_service_test.rb |
Tests Ruby service behavior. |
ruby/lib/basecamp/generated/types.rb |
Refreshes generated types metadata. |
ruby/lib/basecamp/generated/services/bubble_ups_service.rb |
Implements Ruby service. |
ruby/lib/basecamp/generated/metadata.json |
Adds Ruby retry metadata. |
ruby/lib/basecamp/client.rb |
Adds Ruby accessor. |
python/tests/services/test_bubble_ups_service.py |
Tests synchronous Python service. |
python/src/basecamp/generated/types.py |
Adds Python request type. |
python/src/basecamp/generated/services/bubble_ups.py |
Implements sync/async services. |
python/src/basecamp/generated/services/__init__.py |
Exports generated services. |
python/src/basecamp/generated/metadata.json |
Adds Python retry metadata. |
python/src/basecamp/client.py |
Adds synchronous accessor. |
python/src/basecamp/async_client.py |
Adds asynchronous accessor. |
openapi.json |
Adds canonical API definitions. |
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/Types.kt |
Adds Kotlin request body. |
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/bubble-ups.kt |
Implements Kotlin service. |
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/ServiceAccessors.kt |
Adds Kotlin accessor. |
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/Metadata.kt |
Adds Kotlin retry metadata. |
kotlin/README.md |
Updates service counts. |
kotlin/conformance/src/main/kotlin/com/basecamp/sdk/conformance/Main.kt |
Dispatches Kotlin conformance cases. |
go/pkg/generated/client.gen.go |
Adds generated Go operations. |
go/pkg/basecamp/url-routes.json |
Registers Go route metadata. |
go/pkg/basecamp/client.go |
Adds Go service accessor. |
go/pkg/basecamp/bubble_ups.go |
Implements Go wrapper. |
go/pkg/basecamp/bubble_ups_test.go |
Tests Go wrapper behavior. |
go/grouped-client-inventory.yml |
Accounts for new Go operations. |
conformance/tests/paths.json |
Adds route and payload cases. |
conformance/tests/idempotency.json |
Adds retry cases. |
conformance/runner/typescript/runner.test.ts |
Dispatches TypeScript cases. |
conformance/runner/swift/Sources/ConformanceRunner/Dispatch.swift |
Dispatches Swift cases. |
conformance/runner/ruby/runner.rb |
Dispatches Ruby cases and waivers. |
conformance/runner/python/runner.py |
Dispatches Python cases. |
conformance/runner/go/main.go |
Dispatches Go cases. |
behavior-model.json |
Classifies retry behavior. |
AGENTS.md |
Updates operation totals. |
Review details
Suppressed comments (1)
conformance/tests/paths.json:1329
- Despite this case's name and description, its assertions never inspect the request body, so an SDK that serializes
at: nullwould pass. AddrequestBodyAbsentto make the advertised six-runner omission check effective.
"name": "CreateBubbleUp omits 'at' from the wire when not supplied",
"description": "Verifies CreateBubbleUp POSTs /recordings/{recordingId}/bubble_up.json and leaves 'at' off the body when the caller supplies none (the SDK does not force a value). Accepts 204.",
- Files reviewed: 35/57 changed files
- Comments generated: 5
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4b2a7cddbd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
8 issues found across 57 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="python/tests/services/test_bubble_ups_service.py">
<violation number="1" location="python/tests/services/test_bubble_ups_service.py:17">
P3: This new test file covers only the sync BubbleUpsService, but the PR also adds AsyncBubbleUpsService with the same create_bubble_up/delete_bubble_up methods. Sibling service tests (e.g. test_campfires_service.py, test_cards.py) exercise the async client too. Add async equivalents (via AsyncClient(...).for_account(...).bubble_ups) so the generated async surface stays covered.</violation>
</file>
<file name="SECURITY.md">
<violation number="1" location="SECURITY.md:188">
P3: The new count of 10 idempotent POSTs on line 188 leaves the unchanged reference on line 194 ('the 9 flagged POSTs') stale, so the same document now contradicts itself. Update line 194 to 'the 10 flagged POSTs' to match the new count.</violation>
</file>
<file name="conformance/tests/paths.json">
<violation number="1" location="conformance/tests/paths.json:1298">
P2: This test sends `at` in requestBody and its description says it verifies the optional `at` schedule, but the assertions never check that `at` reached the wire. A SDK that drops the field (or serializes it wrong) would pass. Add a `requestBody` assertion with `path: "at"` and `expected: "2026-09-10T09:00:00Z"` to pin the serialization.</violation>
<violation number="2" location="conformance/tests/paths.json:1329">
P2: This test exists solely to pin that the SDK omits `at` from the wire when the caller supplies none, but its assertions (requestPath + noError) never inspect the request body. An SDK that forces `at` onto the body would still pass, so the claimed behavior is unverified. Add a `requestBodyAbsent` assertion with `path: "at"` so the six-SDK omission is actually enforced.</violation>
</file>
<file name="SPEC.md">
<violation number="1" location="SPEC.md:4221">
P3: Appendix E's breakdown no longer matches its own total or behavior-model.json. The total was bumped to 256 but 'Idempotent: 86' still reflects the old 254-operation count; the two new idempotent operations (CreateBubbleUp, DeleteBubbleUp) make it 88, so 86+168=254 contradicts the 256 just stated above. Update the idempotent count to 88.</violation>
<violation number="2" location="SPEC.md:4393">
P3: The per-SDK Service Coverage table is now internally inconsistent. bubbleUps is wired into all six SDKs, but only the Swift and Kotlin rows were bumped to 54; Ruby, TypeScript, and Python still claim 53 'full canonical set', and Go still reads '51 accessors ... Capability is 53/53' (now 52 accessors, capability 54/54). Update the four stale rows to match.</violation>
</file>
<file name="spec/bc3-route-allowlist.yml">
<violation number="1" location="spec/bc3-route-allowlist.yml:130">
P3: The new entries cite line numbers for the same concern/block that contradict the existing position entries. The new POST/DELETE entries say concern :recording_actions (:206) is 'included flat at :276 inside `resources :recordings, only: []` (:261...)' and 'bucket-scoped at :918', while the position entries in the same file describe the same include site as :273/:258/:902. A concern include is a single line, so it can't be both. Reconcile the numbers against the pinned bc3 revision (update whichever set is stale) so the evidence a reader checks is internally consistent.</violation>
</file>
<file name="spec/api-gaps/recording-bubble-up-write.md">
<violation number="1" location="spec/api-gaps/recording-bubble-up-write.md:111">
P3: Rename this case summary to `create-without-`at``; the corresponding `paths.json` fixture omits `at` rather than sending `"now"`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Conformance: assert `at` reaches the wire on the scheduled create case and is absent on the omit case (requestBody / requestBodyAbsent), so a runner that drops or forces the field can no longer pass vacuously. Python: add async create/delete coverage (AsyncClient.bubble_ups) mirroring the sync tests, matching sibling recording-mutation service tests. Docs: idempotent operation count 86 -> 88 (SPEC.md Appendix E); per-SDK service coverage 53 -> 54 for Ruby/TypeScript/Python and 51 -> 52 accessors (54/54 capability) for Go; SECURITY.md "9 flagged POSTs" -> 10; api-gap case name create-now -> create-without-`at` to match the fixture. Route allowlist: reconcile the recording_actions concern-include line numbers (flat :276 / recordings-only-[] :261 / bucket :918 / sibling door :963) against the current bc3 pin 88549ca6; the pre-existing position entries still cited the prior pin, contradicting the correct bubble-up entries.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e03865568c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
What
Adds the per-recording write side of Bubble Up (the BC5 successor to
"save") to all six SDKs. Until now only the read side existed —
GetBubbleUpsmodels the account-user
/my/readings/bubble_ups.jsonlist; there was no way tobubble a single recording up or pop it back down.
Two operations, both answering 204 No Content with no body from
Recordings::BubbleUpsController:CreateBubbleUp→POST /recordings/{recordingId}/bubble_up.json— withan optional
attiming field.DeleteBubbleUp→DELETE /recordings/{recordingId}/bubble_up.json.Both are naturally idempotent (bubbling an already-bubbled recording, or popping
an absent one, is a set-membership no-op that still answers 204). This mirrors
the
DeleteBookmarkprecedent, which ships off the identical 204 shape, andthe
PinMessage/UnpinMessagepair (POST/DELETE 204, empty output).New
BubbleUpstag →BubbleUpsServiceacross all six SDKs via the defaulttag→service fallback (zero generator overrides). Go gets the hand-written
wrapper +
AccountClient.BubbleUps()accessor; TS/Ruby/Python get theirhand-written client accessors; Swift/Kotlin accessors are generated.
The
atfield — a bc3 behavior findingatcontrols timing."now"bubbles up immediately; a scheduling keyword(
"today","tomorrow","weekend","next_week") or an ISO8601 date(e.g.
"2026-09-10") schedules it to resurface later.bc3 currently requires
at.Reading::Bubbleupable#bubble_uproutes"now"tobubble_up_now, but every other value — including anilfrom anomitted param — flows to
Reading::BubbleUpSchedule#bubble_up_at=, whoseelsebranch calls
Date.iso8601(value), andDate.iso8601(nil)raises. So theoperator-verified "optional, now-vs-scheduled" contract holds for scheduling but
omitting
aterrors server-side today — callers must send"now"for theimmediate case. The field is still modeled optional (not
@required) so aone-line bc3 default (
params[:at] ||= "now") would make omission mean "now"with no SDK change. Flagged as a suggested bc3 follow-up in the gap entry.
Route spelling
Modeled at the canonical flat
/recordings/{id}/bubble_up.json, consistentwith the whole
recording_actionsfamily the SDK already ships flat (bookmark,spotlight, pin, position).
resource :bubble_up(bc3config/routes.rb:232)sits in
concern :recording_actions, included flat at the CANONICALresources :recordings(:276) and separately bucket-scoped at:918. Thesibling
resource :positionin the same concern has atest/apitest provingthe concern's flat routes answer on the API host.
bc3's
doc/apidocuments neither spelling yet, so the two routes are waivedin
spec/bc3-route-allowlist.yml(sdk_routes_absent_from_bc3_docs) withroutes.rb+ controller evidence. Documenting the create/destroy contract inbc3
doc/apiwould let both waivers drop.Remaining API gap — per-recording status GET
GET /recordings/{id}/bubble_up.json(theshowaction) is not absorbableand stays a gap.
Recordings::BubbleUpsController#showrendersapp/views/recordings/bubble_ups/show.json.jbuilder, which lives outsideapp/views/api/;restrict_view_paths_to_api_rootlimits the API host to thatroot, so the template is unrenderable there — the same trap that condemned
GetRecording. Thehead :no_contentcreate/destroy sidestep it (no template).Tracked as
partial-coverageinspec/api-gaps/recording-bubble-up-write.md, with the bc3 fix noted (add anapp/views/api/recordings/bubble_ups/show.json.jbuilder, then aGetBubbleUpop +
BubbleUpStatusshape close it).Tests & checks
paths.json— create-with-at,create-omitting-
at(asserts the SDK leaves it off the wire), delete;idempotency.json— both retry on 503 (Ruby GET-only-retry skips recorded inzero-skip-roster.yml).Python / TypeScript service tests.
smithybuild, six-languagemake generate, all parity gates(idempotency, write-semantics, retry-metadata, service-inventory,
operation-assignment, bucket-flat, grouped-client, bc3-route-parity,
validate-api-gaps, doc-constants), Go/TS/Ruby/Python/Kotlin drift + unit +
conformance, Swift drift + SDK/runner build.
Line Tools lack
XCTest— SDK and conformance runner both compile clean) andlint-actions(a local zizmor version flags pre-existing.github/workflowsuses: ./…findings identically onorigin/main; no workflow files changed).Both pass in CI.
Release note: Features (
enhancement).Tracks Basecamp card 10264451804.
Summary by cubic
Adds the per-recording write side of Bubble Up to all six SDKs:
CreateBubbleUp(POST) andDeleteBubbleUp(DELETE) at/recordings/{recordingId}/bubble_up.json. Previously only the read side existed (GetBubbleUps); now a recording can be bubbled up and popped back down.Both operations return 204 with no body and are naturally idempotent, so retrying on 503 is safe.
CreateBubbleUpaccepts an optionalattiming field; bc3 currently rejects an omittedatserver-side, so send"now"for immediate bubble-up. The field is modeled optional so a future bc3 default could make omission mean "now" without SDK changes.New Features
BubbleUpsservice to Go, TypeScript, Ruby, Python, Swift, and Kotlin, with Python async coverage.atreaches the wire when provided and is absent when omitted; adds idempotency coverage and unit tests for create (with and withoutat) and delete.Remaining gap
GET /recordings/{recordingId}/bubble_up.jsonstays uncovered because bc3'sshowtemplate is outside the API view path; tracked as partial coverage.doc/apidoesn't document them yet.Written for commit e038655. Summary will update on new commits.