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
27 changes: 24 additions & 3 deletions conformance/runner/typescript/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ interface TestCase {

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const TESTS_DIR = path.resolve(__dirname, "../../tests");

/**
* Allowance for Node timers firing early. libuv rounds a timer's deadline down
* internally, so `setTimeout(2000)` can elapse in ~1999.9ms of wall clock.
* Small enough that no real backoff regression fits inside it.
*/
const TIMER_SLACK_MS = 2;
const TEST_ACCOUNT_ID = "999";

/**
Expand Down Expand Up @@ -468,7 +475,11 @@ function installMockHandlers(tc: TestCase): {
);
const handler = http.all(originPattern, async ({ request }) => {
count++;
times.push(Date.now());
// performance.now(), not Date.now(): Date.now() is floored to whole
// milliseconds, so bracketing a 2000ms sleep can read 1999 purely from
// rounding. See TIMER_SLACK_MS for the second, separate reason this
// assertion needs care.
times.push(performance.now());
const url = new URL(request.url);
paths.push(url.pathname);
methods.push(request.method);
Expand Down Expand Up @@ -707,10 +718,20 @@ function checkAssertions(
if (times.length >= 2) {
const delay = times[1]! - times[0]!;
const minDelay = assertion.min ?? 0;
// Node's timers may fire marginally BEFORE the requested delay —
// libuv rounds the deadline down internally, so a 2000ms sleep can
// legitimately elapse in 1999.87ms. That is a runtime property, not
// an SDK behaviour: the SDK asked for the full interval. The Go
// runner never sees it because Go's timers do not fire early.
//
// A sub-millisecond allowance cannot mask a real regression, which
// would miss by hundreds of milliseconds (a dropped Retry-After
// means ~1000ms of backoff instead of 2000ms) or by the whole
// interval (no delay at all).
expect(
delay,
`[${tc.name}] expected delay >= ${minDelay}ms, got ${delay}ms`,
).toBeGreaterThanOrEqual(minDelay);
`[${tc.name}] expected delay >= ${minDelay}ms (allowing ${TIMER_SLACK_MS}ms timer slack), got ${delay}ms`,
).toBeGreaterThanOrEqual(minDelay - TIMER_SLACK_MS);
}
break;
}
Expand Down
9 changes: 8 additions & 1 deletion go/pkg/basecamp/recordings.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ type Recording struct {
URL string `json:"url"`
AppURL string `json:"app_url"`
BookmarkURL string `json:"bookmark_url"`
Content string `json:"content,omitempty"`
// BubbleUpURL is the URL of the Bubble Up record for this recording. Optional
// on this polymorphic projection: recordings/_recording.json.jbuilder emits
// the key only when the caller passes bubbleupable, and todolists/_todolist
// is the only partial that does — so a Todolist-shaped instance carries it
// and the other recording types do not.
BubbleUpURL string `json:"bubble_up_url,omitempty"`
Content string `json:"content,omitempty"`
// ContentAttachments and DescriptionAttachments are the rich text companion
// arrays carried through the generic recording projection. A given recording
// is one type, so it carries only the array matching its rich text attribute
Expand Down Expand Up @@ -436,6 +442,7 @@ func recordingFromGenerated(gr generated.Recording) Recording {
URL: gr.Url,
AppURL: gr.AppUrl,
BookmarkURL: gr.BookmarkUrl,
BubbleUpURL: gr.BubbleUpUrl,
Content: gr.Content,
CommentsCount: int(gr.CommentsCount),
CommentsURL: gr.CommentsUrl,
Expand Down
17 changes: 15 additions & 2 deletions go/pkg/basecamp/recordings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,21 @@ func TestRecording_UnmarshalList(t *testing.T) {
t.Fatalf("failed to unmarshal list.json: %v", err)
}

if len(recordings) != 2 {
t.Errorf("expected 2 recordings, got %d", len(recordings))
if len(recordings) != 3 {
t.Errorf("expected 3 recordings, got %d", len(recordings))
}

// bubble_up_url is optional on the generic Recording projection: only the
// todolist partial passes bubbleupable, so the two Message recordings omit
// it and the Todolist one carries it.
for _, r := range recordings {
if r.Type == "Todolist" {
if r.BubbleUpURL == "" {
t.Errorf("Todolist recording %d: expected BubbleUpURL to be set", r.ID)
}
} else if r.BubbleUpURL != "" {
t.Errorf("%s recording %d: expected no BubbleUpURL, got %q", r.Type, r.ID, r.BubbleUpURL)
}
}

// Verify first recording
Expand Down
13 changes: 10 additions & 3 deletions go/pkg/basecamp/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@ type SearchResult struct {
URL string `json:"url"`
AppURL string `json:"app_url"`
BookmarkURL string `json:"bookmark_url"`
Parent *Parent `json:"parent,omitempty"`
Bucket *Bucket `json:"bucket,omitempty"`
Creator *Person `json:"creator,omitempty"`
// BubbleUpURL is the URL of the Bubble Up record for this recording. Optional
// on this polymorphic projection: recordings/_recording.json.jbuilder emits
// the key only when the caller passes bubbleupable, and todolists/_todolist
// is the only partial that does — so a Todolist-shaped instance carries it
// and the other recording types do not.
BubbleUpURL string `json:"bubble_up_url,omitempty"`
Parent *Parent `json:"parent,omitempty"`
Bucket *Bucket `json:"bucket,omitempty"`
Creator *Person `json:"creator,omitempty"`
// Content and Description are always present on the wire and always null:
// api/searches/show.json.jbuilder renders the recording's own partial and
// then unconditionally overwrites both with nil to keep the large HTML body
Expand Down Expand Up @@ -341,6 +347,7 @@ func searchResultFromGenerated(gsr generated.SearchResult) SearchResult {
URL: gsr.Url,
AppURL: gsr.AppUrl,
BookmarkURL: gsr.BookmarkUrl,
BubbleUpURL: gsr.BubbleUpUrl,
Content: gsr.Content,
Description: gsr.Description,
PlainTextContent: gsr.PlainTextContent,
Expand Down
22 changes: 18 additions & 4 deletions go/pkg/basecamp/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,22 @@ func TestSearchResult_UnmarshalResults(t *testing.T) {
t.Fatalf("failed to unmarshal results.json: %v", err)
}

if len(results) != 3 {
t.Errorf("expected 3 results, got %d", len(results))
if len(results) != 4 {
t.Errorf("expected 4 results, got %d", len(results))
}

// bubble_up_url rides the polymorphic search projection: Todolists are
// searchable and api_search_result_template_path falls through to
// todolists/todolist, which passes bubbleupable: true. Every other result
// type omits the key.
for _, r := range results {
if r.Type == "Todolist" {
if r.BubbleUpURL == "" {
t.Errorf("Todolist result %d: expected BubbleUpURL to be set", r.ID)
}
} else if r.BubbleUpURL != "" {
t.Errorf("%s result %d: expected no BubbleUpURL, got %q", r.Type, r.ID, r.BubbleUpURL)
}
}

// Verify first result (Message)
Expand Down Expand Up @@ -515,8 +529,8 @@ func TestSearchService_Search_BestMatchSort(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result.Results) != 3 {
t.Errorf("expected 3 results, got %d", len(result.Results))
if len(result.Results) != 4 {
t.Errorf("expected 4 results, got %d", len(result.Results))
}

// End-to-end projection proof: the polymorphic search projection carries a
Expand Down
28 changes: 17 additions & 11 deletions go/pkg/basecamp/todolist_groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,22 @@ type TodolistGroup struct {
AppURL string `json:"app_url"`
BookmarkURL string `json:"bookmark_url"`
SubscriptionURL string `json:"subscription_url"`
CommentsCount int `json:"comments_count"`
CommentsURL string `json:"comments_url"`
Position int `json:"position"`
Parent *Parent `json:"parent,omitempty"`
Bucket *Bucket `json:"bucket,omitempty"`
Creator *Person `json:"creator,omitempty"`
Name string `json:"name"`
Completed bool `json:"completed"`
CompletedRatio string `json:"completed_ratio"`
TodosURL string `json:"todos_url"`
AppTodosURL string `json:"app_todos_url"`
// BubbleUpURL is the URL of the Bubble Up record for this recording. Always
// present: todolists/_todolist.json.jbuilder renders the shared recording
// partial with bubbleupable: true unconditionally, and every list, show, and
// group path renders that partial.
BubbleUpURL string `json:"bubble_up_url"`
CommentsCount int `json:"comments_count"`
CommentsURL string `json:"comments_url"`
Position int `json:"position"`
Parent *Parent `json:"parent,omitempty"`
Bucket *Bucket `json:"bucket,omitempty"`
Creator *Person `json:"creator,omitempty"`
Name string `json:"name"`
Completed bool `json:"completed"`
CompletedRatio string `json:"completed_ratio"`
TodosURL string `json:"todos_url"`
AppTodosURL string `json:"app_todos_url"`
}

// CreateTodolistGroupRequest specifies the parameters for creating a todolist group.
Expand Down Expand Up @@ -353,6 +358,7 @@ func todolistGroupFromGenerated(gg generated.TodolistGroup) TodolistGroup {
AppURL: gg.AppUrl,
BookmarkURL: gg.BookmarkUrl,
SubscriptionURL: gg.SubscriptionUrl,
BubbleUpURL: gg.BubbleUpUrl,
CommentsCount: int(gg.CommentsCount),
CommentsURL: gg.CommentsUrl,
Position: int(gg.Position),
Expand Down
20 changes: 13 additions & 7 deletions go/pkg/basecamp/todolists.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@ type Todolist struct {
BoostsCount int `json:"boosts_count,omitempty"`
BoostsURL string `json:"boosts_url,omitempty"`
SubscriptionURL string `json:"subscription_url"`
CommentsCount int `json:"comments_count"`
CommentsURL string `json:"comments_url"`
Position int `json:"position"`
Parent *Parent `json:"parent,omitempty"`
Bucket *Bucket `json:"bucket,omitempty"`
Creator *Person `json:"creator,omitempty"`
Description string `json:"description"`
// BubbleUpURL is the URL of the Bubble Up record for this recording. Always
// present: todolists/_todolist.json.jbuilder renders the shared recording
// partial with bubbleupable: true unconditionally, and every list, show, and
// group path renders that partial.
BubbleUpURL string `json:"bubble_up_url"`
CommentsCount int `json:"comments_count"`
CommentsURL string `json:"comments_url"`
Position int `json:"position"`
Parent *Parent `json:"parent,omitempty"`
Bucket *Bucket `json:"bucket,omitempty"`
Creator *Person `json:"creator,omitempty"`
Description string `json:"description"`
// DescriptionAttachments holds structured metadata for the downloadable files
// embedded in the rich text Description. @required — the API always sends this
// array (empty when the description has no inline files). No omitempty, so on
Expand Down Expand Up @@ -400,6 +405,7 @@ func todolistFromGenerated(gtl generated.Todolist) Todolist {
BoostsCount: int(gtl.BoostsCount),
BoostsURL: gtl.BoostsUrl,
SubscriptionURL: gtl.SubscriptionUrl,
BubbleUpURL: gtl.BubbleUpUrl,
CommentsCount: int(gtl.CommentsCount),
CommentsURL: gtl.CommentsUrl,
Position: int(gtl.Position),
Expand Down
54 changes: 42 additions & 12 deletions go/pkg/generated/client.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ data class Recording(
val bucket: RecordingBucket,
val creator: Person,
@SerialName("bookmark_url") val bookmarkUrl: String? = null,
@SerialName("bubble_up_url") val bubbleUpUrl: String? = null,
val content: String? = null,
@SerialName("content_attachments") val contentAttachments: List<RichTextAttachment>? = null,
@SerialName("description_attachments") val descriptionAttachments: List<RichTextAttachment>? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ data class Todolist(
val type: String,
val url: String,
@SerialName("app_url") val appUrl: String,
@SerialName("bubble_up_url") val bubbleUpUrl: String,
val parent: TodoParent,
val bucket: TodoBucket,
val creator: Person,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ data class TodolistGroup(
val type: String,
val url: String,
@SerialName("app_url") val appUrl: String,
@SerialName("bubble_up_url") val bubbleUpUrl: String,
val parent: TodoParent,
val bucket: TodoBucket,
val creator: Person,
Expand Down
Loading
Loading