From c153019087d5084357b0f0f2c2c3ae42c99ef563 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Mon, 19 Oct 2026 08:58:45 -0500 Subject: [PATCH 1/7] chore: add feeds, video and moderation snippets to runner --- .claude/skills/docs-sync/SKILL.md | 11 +- __tests__/docs-snippets/AGENTS.md | 89 +- __tests__/docs-snippets/README.md | 29 +- __tests__/docs-snippets/docs-snippets-todo.md | 12 + __tests__/docs-snippets/global-setup.ts | 7 +- __tests__/docs-snippets/helpers/call-types.ts | 63 ++ __tests__/docs-snippets/helpers/cleanup.ts | 111 +++ __tests__/docs-snippets/helpers/feeds.ts | 141 +++ __tests__/docs-snippets/helpers/leaks.ts | 289 +++++- __tests__/docs-snippets/helpers/server.ts | 110 +++ __tests__/docs-snippets/helpers/uploads.ts | 19 + __tests__/docs-snippets/helpers/video.ts | 46 + __tests__/docs-snippets/maintenance/sweep.ts | 4 +- .../v3-latest/01-basics/01-quickstart.test.ts | 289 ++++++ .../03-feed_groups/01-feed-groups.test.ts | 445 +++++++++ .../03-feed_groups/02-feed-views.test.ts | 185 ++++ .../03-feed_groups/03-aggregation.test.ts | 184 ++++ .../03-feed_groups/04-custom_ranking.test.ts | 932 ++++++++++++++++++ .../05-activity_selectors.test.ts | 618 ++++++++++++ .../06-notification_feeds.test.ts | 831 ++++++++++++++++ .../07-activity_processors.test.ts | 259 +++++ .../03-feed_groups/08-for_you_feed.test.ts | 187 ++++ .../03-feed_groups/09-stories.test.ts | 321 ++++++ .../03-feed_groups/10-user-interests.test.ts | 78 ++ .../04-feeds-and-follows/01-feeds.test.ts | 495 ++++++++++ .../02-search-feeds.test.ts | 289 ++++++ .../03-visibility.test.ts | 127 +++ .../04-feeds-and-follows/04-follows.test.ts | 489 +++++++++ .../05-changing-feed-visibility.test.ts | 115 +++ .../05-membership-levels.test.ts | 334 +++++++ .../06-feed-members.test.ts | 205 ++++ .../05-activities/01-activities.test.ts | 438 ++++++++ .../02-querying-activities.test.ts | 343 +++++++ .../04-activity-feedback.test.ts | 175 ++++ .../05-activities/05-activity-metrics.test.ts | 323 ++++++ .../06-features/01-reactions.test.ts | 352 +++++++ .../06-features/02-bookmarks.test.ts | 313 ++++++ .../v3-latest/06-features/03-comments.test.ts | 433 ++++++++ .../06-features/04-file_uploads.test.ts | 129 +++ .../06-features/05-user-mentions.test.ts | 164 +++ .../06-features/06-url-previews.test.ts | 266 +++++ .../v3-latest/06-features/07-pins.test.ts | 143 +++ .../v3-latest/06-features/08-polls.test.ts | 492 +++++++++ .../06-features/10-moderation.test.ts | 403 ++++++++ .../06-features/11-collections.test.ts | 361 +++++++ .../12-troubleshooting/01-rate_limits.test.ts | 78 ++ .../14-best_practices/03-cutoff-date.test.ts | 88 ++ .../04-hot-feed-cache.test.ts | 122 +++ .../02-feed_group_push_configuration.test.ts | 72 ++ .../15-push/03-push_template.test.ts | 112 +++ .../01-basics/02-installation.agent.test.ts | 34 + .../node/01-basics/02-installation.test.ts | 44 + .../node/06-features/06-events.test.ts | 64 ++ .../02-file_uploads.standalone.test.ts | 5 +- .../chat/04-messages/02-file_uploads.test.ts | 18 +- .../02-app_settings.test.ts | 4 +- .../02-quick-start-chat-moderation.test.ts | 185 ++++ .../03-quick-start-feeds-moderation.test.ts | 96 ++ .../07-quick-start-call-moderation.test.ts | 249 +++++ ...08-quick-start-feeds-v3-moderation.test.ts | 222 +++++ .../04-configuration/01-policies.test.ts | 169 ++++ .../04-configuration/02-rules.test.ts | 171 ++++ .../04-configuration/04-filters.test.ts | 253 +++++ .../04-configuration/05-evaluations.test.ts | 174 ++++ .../04-configuration/05-flooding.test.ts | 88 ++ .../05-content-moderation/01-check.test.ts | 330 +++++++ .../02-flag-mute-ban.test.ts | 423 ++++++++ .../06-review-queue/01-review-queue.test.ts | 100 ++ .../06-review-queue/02-actions.test.ts | 285 ++++++ .../06-review-queue/03-appeals.test.ts | 393 ++++++++ .../06-review-queue/04-labels.test.ts | 70 ++ .../06-review-queue/05-custom-views.test.ts | 180 ++++ .../01-logs-and-analytics.test.ts | 188 ++++ .../10-multitenancy/01-multitenancy.test.ts | 397 ++++++++ .../11-guides/07-client-configuration.test.ts | 38 + .../09-rule-triggered-webhooks.test.ts | 68 ++ .../01-getting-started.test.ts | 174 ++++ .../01-webhooks.test.ts | 45 +- .../01-basics/01-get_started.agent.test.ts | 29 + .../01-basics/01-get_started.client.test.ts | 44 + .../api/01-basics/01-get_started.test.ts | 216 ++++ .../video/api/01-basics/03-calls.test.ts | 675 +++++++++++++ .../video/api/01-basics/04-ring-calls.test.ts | 220 +++++ .../api/02-call_types/02-manage-types.test.ts | 126 +++ .../api/02-call_types/03-geofencing.test.ts | 87 ++ .../api/02-call_types/03-permissions.test.ts | 129 +++ .../api/02-call_types/05-settings.test.ts | 155 +++ .../api/03-streaming/01-overview.test.ts | 137 +++ .../api/03-streaming/02-backstage.test.ts | 214 ++++ .../video/api/03-streaming/04-rtmp.test.ts | 69 ++ .../video/api/03-streaming/06-srt.test.ts | 86 ++ .../video/api/03-streaming/08-hls.test.ts | 186 ++++ .../03-streaming/09-rtmp_broadcast.test.ts | 177 ++++ .../video/api/04-quality/03-stats.test.ts | 38 + .../api/06-moderation/01-overview.test.ts | 383 +++++++ .../api/07-recording/02-composite.test.ts | 357 +++++++ .../07-recording/03-individual_track.test.ts | 168 ++++ .../video/api/07-recording/04-raw.test.ts | 144 +++ .../api/07-recording/05-audio_only.test.ts | 210 ++++ .../07-recording/06-frame_recording.test.ts | 109 ++ .../07-recording/07-manage_recordings.test.ts | 92 ++ .../video/api/07-recording/08-storage.test.ts | 307 ++++++ .../01-transcribing_calls.test.ts | 407 ++++++++ .../api/08-transcription/02-storage.test.ts | 297 ++++++ .../03-translated-captions.test.ts | 199 ++++ .../api/09-GDPR and Privacy/03-calls.test.ts | 75 ++ .../01-download-user-feedback.test.ts | 304 ++++++ .../12-analytics/03-call-attendance.test.ts | 166 ++++ .../04-participant-metrics.test.ts | 123 +++ __tests__/docs-snippets/setup.ts | 4 +- .../docs-snippets/sync/docs-sync.test.ts | 10 +- __tests__/docs-snippets/sync/parse.ts | 65 +- 112 files changed, 22490 insertions(+), 106 deletions(-) create mode 100644 __tests__/docs-snippets/helpers/call-types.ts create mode 100644 __tests__/docs-snippets/helpers/feeds.ts create mode 100644 __tests__/docs-snippets/helpers/uploads.ts create mode 100644 __tests__/docs-snippets/helpers/video.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/01-basics/01-quickstart.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/01-feed-groups.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/02-feed-views.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/03-aggregation.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/04-custom_ranking.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/05-activity_selectors.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/06-notification_feeds.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/07-activity_processors.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/08-for_you_feed.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/09-stories.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/10-user-interests.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/04-feeds-and-follows/01-feeds.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/04-feeds-and-follows/02-search-feeds.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/04-feeds-and-follows/03-visibility.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/04-feeds-and-follows/04-follows.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/04-feeds-and-follows/05-changing-feed-visibility.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/04-feeds-and-follows/05-membership-levels.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/04-feeds-and-follows/06-feed-members.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/05-activities/01-activities.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/05-activities/02-querying-activities.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/05-activities/04-activity-feedback.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/05-activities/05-activity-metrics.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/01-reactions.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/02-bookmarks.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/03-comments.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/04-file_uploads.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/05-user-mentions.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/06-url-previews.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/07-pins.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/08-polls.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/10-moderation.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/11-collections.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/12-troubleshooting/01-rate_limits.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/14-best_practices/03-cutoff-date.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/14-best_practices/04-hot-feed-cache.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/15-push/02-feed_group_push_configuration.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/15-push/03-push_template.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/node/01-basics/02-installation.agent.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/node/01-basics/02-installation.test.ts create mode 100644 __tests__/docs-snippets/pages/activity-feeds/v3-latest/node/06-features/06-events.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/02-quick-start/02-quick-start-chat-moderation.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/02-quick-start/03-quick-start-feeds-moderation.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/02-quick-start/07-quick-start-call-moderation.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/02-quick-start/08-quick-start-feeds-v3-moderation.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/04-configuration/01-policies.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/04-configuration/02-rules.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/04-configuration/04-filters.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/04-configuration/05-evaluations.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/04-configuration/05-flooding.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/05-content-moderation/01-check.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/05-content-moderation/02-flag-mute-ban.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/06-review-queue/01-review-queue.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/06-review-queue/02-actions.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/06-review-queue/03-appeals.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/06-review-queue/04-labels.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/06-review-queue/05-custom-views.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/09-logs-and-analytics/01-logs-and-analytics.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/10-multitenancy/01-multitenancy.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/11-guides/07-client-configuration.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/11-guides/09-rule-triggered-webhooks.test.ts create mode 100644 __tests__/docs-snippets/pages/moderation/node/01-getting-started/01-getting-started.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/01-basics/01-get_started.agent.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/01-basics/01-get_started.client.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/01-basics/01-get_started.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/01-basics/03-calls.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/01-basics/04-ring-calls.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/02-call_types/02-manage-types.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/02-call_types/03-geofencing.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/02-call_types/03-permissions.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/02-call_types/05-settings.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/03-streaming/01-overview.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/03-streaming/02-backstage.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/03-streaming/04-rtmp.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/03-streaming/06-srt.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/03-streaming/08-hls.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/03-streaming/09-rtmp_broadcast.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/04-quality/03-stats.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/06-moderation/01-overview.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/07-recording/02-composite.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/07-recording/03-individual_track.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/07-recording/04-raw.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/07-recording/05-audio_only.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/07-recording/06-frame_recording.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/07-recording/07-manage_recordings.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/07-recording/08-storage.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/08-transcription/01-transcribing_calls.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/08-transcription/02-storage.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/08-transcription/03-translated-captions.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/09-GDPR and Privacy/03-calls.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/12-analytics/01-download-user-feedback.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/12-analytics/03-call-attendance.test.ts create mode 100644 __tests__/docs-snippets/pages/video/api/12-analytics/04-participant-metrics.test.ts diff --git a/.claude/skills/docs-sync/SKILL.md b/.claude/skills/docs-sync/SKILL.md index a167613..32b8d59 100644 --- a/.claude/skills/docs-sync/SKILL.md +++ b/.claude/skills/docs-sync/SKILL.md @@ -6,7 +6,7 @@ argument-hint: "[path to the getstream.io checkout or its content/docs folder]" # Docs sync -The tests in `__tests__/docs-snippets/` run the server-side `Node.js` snippets of the Stream docs (the chat Node sidebar and the platform sidebar) against a real Stream app with the `@stream-io/node-sdk` server client. Each docs fence has exactly one marked region in a test, and `yarn test:docs:sync` checks that they still agree. This skill makes them agree again after either side changed. +The tests in `__tests__/docs-snippets/` run the server-side `Node.js` snippets of the Stream docs (the chat, activity feeds v3 and moderation Node sidebars, the video API sidebar and the platform sidebar: `SIDEBAR_FILES` in `sync/parse.ts`) against a real Stream app with the `@stream-io/node-sdk` server client. Each docs fence has exactly one marked region in a test, and `yarn test:docs:sync` checks that they still agree. This skill makes them agree again after either side changed. **Docs have preference.** When a region and its fence differ, the docs version is the source of truth: update the test to match it. Change the docs only when the docs snippet doesn't work (type error against the node SDK, runtime error that isn't caused by test setup, wrong request or response shape), and then write the fix back from the test. @@ -49,9 +49,9 @@ Before the sync check, make sure every server-side snippet on the tested pages h ### Scope -- Pages: every page of the `[chat][node].json` and `[platform][default].json` sidebars (`$DOCS_DIR/_sidebars/`) under `chat/` or `platform/`, the same list as `findSidebarPages` in `sync/parse.ts`. +- Pages: every page of the tested sidebars (`SIDEBAR_FILES` in `sync/parse.ts`, `$DOCS_DIR/_sidebars/`) under `PAGE_PREFIXES`, the same list as `findSidebarPages`. - Fences: every JS fence (`js`, `javascript`, `ts`, `typescript`) labelled `JavaScript` or unlabelled, together with its tab group: the other fences in the same `` block, or the fence alone when it isn't in one. -- Out of scope: `React` and `React Native` fences, `Node.js` fences inside a `` block, and comment-only fences. +- Out of scope: `React` and `React Native` fences, `Node.js` fences inside a `` block, fences listed in `EXCLUDED_FENCES` (`sync/parse.ts`), and comment-only fences. List them with a throwaway script in the scratchpad (page, heading, line, the labels in the group, the code). Then classify each group by its tabs first and the prose second: @@ -89,7 +89,7 @@ It runs without network and reports four kinds of failure. Collect them all, gro | Failure | Meaning | | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `snippets without a test region` | A `Node.js` fence on a tested sidebar page (chat Node, platform) has no region: a new snippet, a new page, or a fence whose key changed (see the next row). | +| `snippets without a test region` | A `Node.js` fence on a tested sidebar page (`SIDEBAR_FILES`) has no region: a new snippet, a new page, or a fence whose key changed (see the next row). | | mislabelled JS fences | A JS fence is unlabelled or has a label other than `JavaScript`, `React`, `React Native` or `Node.js`. | | `no such fence` | A region's key (`docs`, `heading`, `tab`, `index`) matches no fence: the fence was removed, moved, or its heading was renamed. The failure lists the page's fence keys. | | ` differs from :` | The region's code and the fence's code differ. | @@ -127,13 +127,14 @@ When a fix lands that resolves an entry in `docs-snippets-todo.md` (the docs fix ### New fence without a region -Write a test for it in the page's test file (create the file if the page is new). The path mirrors the docs path relative to `content/docs/`, without `/_default`: `chat/_default/04-messages/01-send_message.md` → `__tests__/docs-snippets/pages/chat/04-messages/01-send_message.test.ts`. Follow `__tests__/docs-snippets/AGENTS.md` (isolation with `uniqueId`, `Cleanup`, one `it` per snippet, basic `expect`s after each region). In short: +Write a test for it in the page's test file (create the file if the page is new). The path mirrors the docs path relative to `content/docs/`, without `/_default`: `chat/_default/04-messages/01-send_message.md` → `__tests__/docs-snippets/pages/chat/04-messages/01-send_message.test.ts`, `activity-feeds/v3-latest/_default/05-activities/01-activities.md` → `__tests__/docs-snippets/pages/activity-feeds/v3-latest/05-activities/01-activities.test.ts`. Follow `__tests__/docs-snippets/AGENTS.md` (isolation with `uniqueId`, `Cleanup`, one `it` per snippet, basic `expect`s after each region). In short: - Run the snippet with `const client = getServerClient()` (`helpers/clients.ts`), named `client` as in the docs. Users the snippet acts for (`user_id`, `created_by_id`) must exist first, with `uniqueId` ids. - Everything between the markers is the docs code; setup goes before, assertions after. Fragments get the minimal surrounding code to compile and run. - Comment-only fences need no region. - A fence that isn't code (e.g. a bare data shape) is a docs bug: rewrite it as code that runs (e.g. reading and logging the fields), and report it. Never remove a `Node.js` fence just because it is a fragment or can't be tested: wrap it, or put it in `it.skip`. - A snippet the test app can't run goes in `it.skip('BLOCKED: ', ...)` so it is still typechecked; report the exact API error. +- **Another SDK's code in a `Node.js` fence** (e.g. Feeds v2 `getstream` client code: `client.flagUser`, `client.user(id).flag`, `ctx.alice.feed(...)`): it isn't `@stream-io/node-sdk` code and isn't ours to update. Don't rewrite or relabel it: add the fence to `EXCLUDED_FENCES` in `sync/parse.ts` with a reason, and report it. - **Client-side code in a `Node.js` fence** (needs a connected user or a WebSocket, or a method the node SDK doesn't have: `connectUser`, `channel.watch()`, `client.on(...)`): never test different code from what the fence shows. If the node SDK can do the same thing, make the smallest docs fix that turns the fence into that call (`channel.watch()` → `channel.getOrCreate()`, `user_id` instead of `connectUser`). If it can't (events, WebSocket, local client state), the fence is mislabelled: label it `JavaScript`. Use `it.skip('BLOCKED: client-side code: ...')` only while that decision is open. Report it either way. To check what a server-side endpoint accepts, read the SDK source (`src/StreamClient.ts`, `src/StreamChatClient.ts`, `src/StreamChannel.ts`, `src/gen/`) or the OpenAPI spec (`chat-serverside-api.json` in https://github.com/GetStream/protocol/tree/main/openapi/v2). - **Push and webhooks**: don't test that notifications or webhook events arrive; assert the response or read the setting back (`getApp()`, `listPushProviders()`, ...). Never touch or use the test app's own push provider (see "Test app" in `AGENTS.md`): only use providers the test creates with `uniqueId` names. Snippets that need real credentials run in `describe.skipIf(!hasFirebaseCredentials)` (`helpers/push.ts`); the README's "Push credentials" section has the details. - **App-level side effects** (app settings, built-in channel types, grants, webhook URLs, push config, ...): read the original value first and restore it with `cleanup.add(...)`, or the file fails with `DRIFT`. diff --git a/__tests__/docs-snippets/AGENTS.md b/__tests__/docs-snippets/AGENTS.md index 2b977c0..4cc1b1e 100644 --- a/__tests__/docs-snippets/AGENTS.md +++ b/__tests__/docs-snippets/AGENTS.md @@ -11,16 +11,16 @@ New entries are added only after the user approves them (agents propose them in - `@stream-io/node-sdk` is aliased to the repo's `index.ts` (`resolve.alias` in `vitest.config.mts`, `paths` in `tsconfig.json`), so a docs fence that starts with `import ... from "@stream-io/node-sdk"` can be a region at module level: declare the values it uses (e.g. `apiKey`) above it. - The docs call the server client `client`: use `const client = getServerClient()` (`helpers/clients.ts`, 30s timeout) and the same variable names as the docs (`client`, `channel`, ...). `createUserToken(userId)` signs user tokens. - Snippets act on behalf of users with `user_id` / `created_by_id`: those users must exist (`client.upsertUsers`) and have `uniqueId` ids. -- **Isolation**: the id or name of everything a test creates comes from `uniqueId('name')`: users, channels, channel types, roles, commands, blocklists, segments, campaigns (name), user groups, moderation config keys, predefined filters, push providers. Polls are matched by their creator, so create them as a `uniqueId` user. Never use fixed ids like `"john"` or `"general"`. +- **Isolation**: the id or name of everything a test creates comes from `uniqueId('name')`: users, channels, channel types, roles, commands, blocklists, segments, campaigns (name), user groups, moderation config keys, rule names, feed moderation template names, evaluation set names and custom view (queue) names, external storage names, feeds membership level ids, feeds collection names / ids, predefined filters, push providers, call types, calls, feed groups, feeds, feed views. Polls are matched by their creator, so create them as a `uniqueId` user. Never use fixed ids like `"john"` or `"general"`. - **Cleanup**: register everything you create on a `Cleanup` instance and call `cleanup.run()` in `afterAll`. App-level settings changed by a test must be restored with `cleanup.add(...)`. - Tests in one file may share data created in `beforeAll`. Nothing may leak across files. - `cleanup.run()` attempts every step and then throws if anything failed, so leftovers show up as a failed `afterAll` instead of piling up in the app. Server-side hard channel deletion is a background task: `Cleanup` waits for it (`waitForTask`) and retries deleting channel types until their channels are gone. -- **Leak check** (`setup.ts`, runs automatically after each file's own `afterAll`): looks up everything minted by `uniqueId` in that file (users, channels, channel types, commands, roles, custom permissions, blocklists, segments, campaigns, user groups, moderation configs, predefined filters, push providers, polls). Anything still there fails the file with a `LEAK` error listing it, and is then removed. Per-user data (reminders, devices, drafts, ...) goes away with the user. It also snapshots app settings (incl. push and webhook config), pre-existing channel types and retention policies before the file and fails with `DRIFT` if anything changed and wasn't restored. -- **End-of-run sweep** (`global-setup.ts`, after every `yarn test:docs`): a file that throws while loading runs no hooks, so its cleanup and leak check never run. The sweep removes every `docs-test` resource created since the run started (channels are found by listing every channel created since then) and fails the run with a `LEAK` list. It also puts back the retention policies the run started with and fails with `DRIFT` if it had to. Don't run two `yarn test:docs` at once: the sweep would remove the other run's resources. +- **Leak check** (`setup.ts`, runs automatically after each file's own `afterAll`): looks up everything minted by `uniqueId` in that file (users, channels, channel types, commands, roles, custom permissions, blocklists, segments, campaigns, user groups, moderation configs, rules, feed moderation templates, evaluation sets and custom views, external storages, membership levels, feeds collections, predefined filters, push providers, polls, call types, calls, feed groups, feeds, feed views). Anything still there fails the file with a `LEAK` error listing it, and is then removed. Per-user data (reminders, devices, drafts, ...) goes away with the user; activities, comments and reactions with their feed. It also snapshots app settings (incl. push and webhook config), pre-existing channel types and retention policies before the file and fails with `DRIFT` if anything changed and wasn't restored. +- **End-of-run sweep** (`global-setup.ts`, after every `yarn test:docs`): a file that throws while loading runs no hooks, so its cleanup and leak check never run. The sweep removes every `docs-test` resource created since the run started (channels, calls and feeds are found by listing every one created since then) and fails the run with a `LEAK` list. It also puts back the retention policies the run started with and fails with `DRIFT` if it had to. Don't run two `yarn test:docs` at once: the sweep would remove the other run's resources. - Anything created under a non-`uniqueId` name can't be found by the leak check or the sweep. Don't do that. - **Eventual consistency**: channel type changes (`createChannelType` / `updateChannelType`) take ~30s to reach every API node. Call `await waitForChannelTypePropagation()` (from `helpers/wait.ts`) right after creating or updating a channel type. Timeouts are 90s per test and 180s per hook to leave room for this. Use `retry(fn, { retryIf })` for other eventually-consistent reads (e.g. search). Background tasks: `waitForTask(client, taskId)`, and `waitForBatchTask` for channel batch updates (their top-level status completes too early). - **App settings**: pages are run one at a time, so tests may change app settings and built-in channel types, but must restore them (`cleanup.add`). The check after each file fails with `DRIFT` otherwise. -- `updateChannelType(client, name, patch)` (`helpers/channel-types.ts`) fills in the fields the request requires; `grantMessagingMembers` (`helpers/grants.ts`) adds `messaging` member grants and registers the restore. `helpers/server.ts` has repeated lookups: `sendServerMessage`, `getServerUser` / `getServerMember` / `getServerChannel` (throw unless exactly one result matches), `deletePollsCreatedBy`, `waitForAppSetting` and `waitForTeamIdSupport` (multi-tenancy toggles). +- `helpers/call-types.ts`: `createCallType(client, cleanup)` creates a `uniqueId` call type, waits until it reads back, and registers it on `cleanup.callTypes`; `trackCallTypeCreates(client, cleanup)` does the same for fences that create the type themselves (spy, call before the region); `retryOnCallTypeMissing(fn)` retries calls into a new type (successful `getCallType` reads don't prove every node knows it: `getOrCreate` can still fail with code 16 "call type does not exist", wrap it in a spy inside regions). A call type with calls can't be deleted (code 4 "multiple calls of that type exist"): `Cleanup` hard-deletes `cleanup.calls` (waiting for the delete task) before `cleanup.callTypes`. `updateChannelType(client, name, patch)` (`helpers/channel-types.ts`) fills in the fields the request requires; `grantMessagingMembers` (`helpers/grants.ts`) adds `messaging` member grants and registers the restore. `helpers/server.ts` has repeated lookups: `sendServerMessage`, `getServerUser` / `getServerMember` / `getServerChannel` (throw unless exactly one result matches), `deletePollsCreatedBy`, `waitForAppSetting`, `waitForTeamIdSupport` (multi-tenancy toggles) `waitForReviewItem` (review queue item of an entity once its flags are there), `deleteFeedModerationTemplate` (the generated `v2DeleteTemplate()` can't send the name) and `saveAndRestoreEventHooks(client, cleanup)` (saves `event_hooks`, registers the restore, returns `{ original, restore }`; `waitForEventHooks`, `withoutTimestamps`). `helpers/feeds.ts`: `retryOnFeedGroupMissing`, `addActivityOnce`, `copyFeedGroup(client, cleanup, builtInId)` (a `uniqueId` group with a built-in group's settings, on `cleanup.feedGroups`), `notificationTypes` / `waitForNotificationTypes` (a user's built-in `notification` feed). `helpers/uploads.ts`: upload fixtures (`JPEG_BASE64` / `jpeg`, `MINIMAL_PDF`) and `ignoreMissingFile` (swallows the 404 of deleting a gone upload). - One `it` per docs snippet (or per closely related group of snippets in the same tab group). - Add basic `expect`s after each snippet to prove it did what the docs claim. @@ -46,6 +46,7 @@ New entries are added only after the user approves them (agents propose them in - `StreamFeedsClient`'s deprecated wrappers (`addReaction`, `followBatch`, `followUnbatch`) call `super.`, so a spy on the new method (`addActivityReaction`) doesn't see calls made through the wrapper: spy on the wrapper itself. - `client.feeds.feed(group, id)` returns a `StreamFeed` that calls the same `client.feeds` instance, so `vi.spyOn(client.feeds, 'updateFeedMembers')` also sees calls made through the feed object. +- A fence where one mid-fence call can't run on the test app (e.g. "there is no active session") can still run in a normal `it`: spy on that generated method (`client.video.*`; `StreamCall` methods go through it) with a `mockImplementation` that records the error and returns `undefined as never`, assert the error and the other calls. Bind the original before `vi.spyOn` (binding after it binds the spy and recurses). - To check state between calls inside a region (e.g. after each of several `hide()` calls), spy on the generated method (`client.chat.hideChannel`) with a `mockImplementation` that awaits the bound original, records a probe query, and returns the response. The region stays as is, and `mock.calls[i][0]` is typed (no cast). - To tell an API behavior from a snippet bug, write a throwaway probe test under `pages/` that logs the results of request variants (e.g. with and without a flag). It still runs the leak check, so name and register what it creates; delete the file before finishing. @@ -57,6 +58,7 @@ New entries are added only after the user approves them (agents propose them in - A snippet that assigns a result it never reads (`const update = await channel.update(...)`) fails `unused-imports/no-unused-vars`: assert on that variable after the region instead of disabling the rule. - A docs placeholder that has to fit a string-literal union (e.g. `push_provider: "apn or firebase or huawei or xiaomi"`) can stay in the docs: COPY-map it to a variable with a valid literal type (`const pushProvider = 'firebase'`). +- A COPY value can be a backtick literal (``id=`my-story` ``) to match a fence's template literal. Don't name a COPY variable after an identifier the fence also uses as an object (`john` with `john.id`): use `johnId` and declare `const john = { id: johnId }`. - COPY replaces the identifier everywhere in the region that isn't a property access or object key, including comments and strings (`sender="..."` also rewrote `{{sender.id}}`). Pick COPY names that appear nowhere else in the fence (`campaignSender`). - `DOCS_SYNC_WRITE` keeps line breaks that the test's prettier added because of longer COPY names, and docs prettier doesn't rejoin them. Check `git diff` after writing and rejoin untouched lines by hand (the sync check ignores wrapping). - To read the result of a call the region doesn't keep, spy on the method and `await` its `mock.results[0].value` (a promise for async methods), e.g. `client.updateLiveLocation`. @@ -85,7 +87,8 @@ New entries are added only after the user approves them (agents propose them in - A fence that builds a request object in a separate `const` and passes it later fails TS2322 when a field is a string-literal union (`callback.mode: "CALLBACK_MODE_REST"` widens to `string`). The plain-JS fix is to move the object inline into the call. - `updateChannelType` returns `commands` as names (`string[]`), while `getChannelType` / `listChannelTypes` return `Command` objects: compare by `.name`. `commands: ["all"]` expands to every built-in command (giphy, ban, unban, mute, unmute). - A `vi.spyOn(client, 'updateApp').mockImplementation(...)` must take `request?: UpdateAppRequest` (optional), or it fails TS2345. -- A correct fence that the SDK types reject can't be parked with `it.skip`: skipped bodies are still typechecked, so `yarn types:docs` fails for everyone. Stop and add it to the TODO's API spec / generator issues. The generator drops `nullable` everywhere (no `| null` in `src/gen/models`), e.g. `grants: null` on `updateChannelType` fails TS2322. +- A fence calling a method the node SDK doesn't have (left out of the spec, TS2339) also fails eslint's `no-unsafe-*` rules, which would break `yarn lint` in CI: put it in `it.skip('BLOCKED: SDK issue: ...')` and type the missing method with a narrow local cast on the client (`getServerClient() as StreamClient & { moderation: { method: (req: {...}) => Promise<{...}> } }`), with a comment, and list it in the TODO. The region stays as the docs have it. +- A correct fence that the SDK types reject can't be parked with `it.skip`: skipped bodies are still typechecked. Keep the region anyway (in a running `it` if it works at runtime, else `it.skip('BLOCKED: SDK issue: ...')`) so coverage passes, and add it to the TODO's API spec / generator issues: `yarn types:docs` reports it until the SDK is fixed, and the TODO lists every such known error. The generator drops `nullable` everywhere (no `| null` in `src/gen/models`), e.g. `grants: null` on `updateChannelType` fails TS2322. - `verifyAndParseWebhook` fences: build `req = { rawBody, headers: { 'x-signature': sig } }` before the region, where `sig` is the HMAC-SHA256 hex of the uncompressed JSON keyed with `STREAM_SECRET` and `rawBody` is the JSON or `zlib.gzipSync(json)` (the signature always covers the decompressed bytes). - Optional response fields (`GetTaskResponse.result`, `MessageResponse.i18n`) aren't narrowed by a status check: a fence reading `response.result.url` fails TS18048, and the docs fix is `?.`. - `getApp().app.enforce_unique_usernames` is typed `string` but `updateApp` expects `'no' | 'app' | 'team'`: cast the saved value when restoring it. `"team"` is accepted with multi-tenancy off. @@ -100,6 +103,7 @@ New entries are added only after the user approves them (agents propose them in - A fence that reuses the same placeholder ids for a call that needs a different resource (e.g. `deleteChannels({ cids: [cid1, cid2], skip_truncate: true })` after hard-deleting `cid1`/`cid2`, "distinct channels only") is a docs bug: give that call its own ids (`distinctCid1`, `distinctCid2`). Don't swap request fields in a spy to make it pass. +- A `Node.js` fence with another SDK's code (Feeds v2 `getstream` client code, e.g. on the moderation Feeds v2 quick start) isn't ours to update: list it in `EXCLUDED_FENCES` (`sync/parse.ts`) with a reason instead of rewriting or relabelling it. - Undefined app-supplied values in a fence (`userId`, `channelId`, ...) aren't docs bugs: define them in the test just before the region and leave the docs as they are. - A docs fix is the code change only: don't add explanatory comments to the fence (e.g. why `user_id` is needed). Put the reason in the report. @@ -128,9 +132,34 @@ New entries are added only after the user approves them (agents propose them in - SQS / SNS hooks with placeholder keys are accepted; `checkSQS` / `checkSNS` with fake keys resolve with `status: "error"` and an AWS "InvalidClientTokenId" message instead of rejecting (spy on the method to assert the result). Failover configs are validated on save: placeholder GCS credentials fail with code 4, and `type: "s3"` is not enabled on the test app (code 4). Probe with one `updateApp` call before writing setup. - `waitForChannelTypePropagation()` waits only for what changed since its last call (`getServerClient()` records channel type creates and updates): a new type ≥10s after creation plus 10 agreeing lookups, an update of a docs-test type 10s, an update of a built-in or pre-existing type 30s (the DRIFT check reads those back). Measured 2026-10-13: updates are enforced everywhere within ~3s, but `getChannelType` / `listChannelTypes` reads can return the old value for over a minute, so read a type back with a retry (`waitForChannelType`-style) or assert the update response. A test with two waits on a built-in type needs an explicit timeout above the 90s default. - An app setting that changes API behavior (e.g. `enforce_unique_usernames`) can break later tests in the same file: restore it at the end of the test that changes it, and keep the `cleanup.add` restore as a fallback. -- The leak check doesn't track video calls: create them with `uniqueId` ids and `cleanup.add(() => client.video.call(type, id).delete({ hard: true }))`, ignoring "not found". -- Feeds v3 works on the test app (feed groups `user`, `timeline`, `notification`, ...), but the leak check and the end-of-run sweep don't track feeds: create them with `uniqueId` ids (`client.feeds.feed(group, id).getOrCreate({ user_id })`) and `cleanup.add(() => feed.delete({ hard_delete: true }))` (ignore "not found" when a snippet may already have deleted it). After a soft `deleteFeedUserData`, `getActivity` on the user's activity rejects with `metadata.responseCode: 404`. -- Feed groups are app-wide and eventually consistent: create them with a `uniqueId` id and `cleanup.add(() => client.feeds.deleteFeedGroup({ id, hard_delete: true }))`. Calls into a new group can fail with code 16 "Feed group with ID ... not found" for a while: spy on the call with a `mockImplementation` that retries the bound original on "not found". +- Video calls: create them with `uniqueId` ids and push the cid to `cleanup.calls` (hard deleted, "not found" ignored). Call types: `cleanup.callTypes` (see `helpers/call-types.ts`). +- Feeds v3 works on the test app (feed groups `user`, `timeline`, `notification`, ...): create feeds with `uniqueId` ids (`client.feeds.feed(group, id).getOrCreate({ user_id })`) and push the fid (`group:id`) to `cleanup.feeds` (hard deleted, "not found" / "can't be found" ignored, so a snippet may delete it first). After a soft `deleteFeedUserData`, `getActivity` on the user's activity rejects with `metadata.responseCode: 404`. +- Feed groups are app-wide and eventually consistent: create them with a `uniqueId` id (hyphens are accepted) and push it to `cleanup.feedGroups` (deleted after `cleanup.feeds`). Calls into a new group can fail with code 16 "Feed group with ID ... not found" for a while (usually 1–2s): `retryOnFeedGroupMissing(client, ['getOrCreateFeed', 'follow', ...])` (`helpers/feeds.ts`) spies on the generated methods and retries them (`StreamFeed` methods go through `client.feeds`). `addActivity` into a new group can answer "not found" after storing the activity, so a blind retry adds it twice: in setup use `addActivityOnce(client, request)` (explicit id, retries only while `getActivity` doesn't find it). `updateFeedGroup` changes take a few seconds to reach reads: wait for several reads in a row to agree. An `updateFeedGroup` fence that leaves fields out clears them, including `aggregation` and `push_notification.push_types`. Feed group `ranking.type` accepts only `expression` and `interest` (code 4); recency is the default when `ranking` is omitted. `updateFeedGroup({ id })` with no other fields clears `custom` (updates replace every field). `deleteFeedGroup` without `hard_delete` soft-deletes (listed only with `include_soft_deleted: true`). +- Feed views are app-wide and eventually consistent: `uniqueId` ids and `cleanup.add(() => client.feeds.deleteFeedView({ id }))` (ignore "not found"). `getFeedView` and `getOrCreate({ view })` answer code 16 "... not found" for 1–3s after a create (`retryOnFeedGroupMissing` / `isFeedGroupMissing` match it), `listFeedViews` lags ~4s, `ranking.type: "recency"` is rejected, `updateFeedView({ id })` replaces every field. To read a spy that `retryOnFeedGroupMissing` installed, call `vi.spyOn(client.feeds, 'method')` again (returns the existing mock); `vi.mocked(client.feeds.method)` fails `unbound-method`. +- `updateApp({ max_aggregated_activities_length })` returns 200 but `getApp` keeps 0 and the limit isn't applied (2026-10-16); values over 100 are rejected (code 4). +- Feed ranking: ranked results settle within ~1–3s; make test popularity the reverse of recency so a passing order proves the ranking. Popularity = reactions (one per type per user: use several types) + shares × 3. Ranking expressions are validated on create (unknown variables such as `externals`, fields without defaults, non-map arguments to map functions, `min_popularity` < 1 for the popular selector). `preference_score` is 0 until the user gives `activityFeedback`. A group created without `activity_selectors` defaults to `current_feed`. Map key order in responses (`ranking.functions`) isn't stable: sort before comparing. A duplicate `createFeedGroup` id (or a built-in id like `timeline`) fails with "already exists": a docs bug. +- Popularity = reactions + comments ×2 + bookmarks ×3 + shares ×3: `addCommentsBatch` from one user is the cheapest way to a high popularity (35 comments = 70). A proximity selector group needs a `near` / `within_bounds` filter on read (code 4 for a user without a location); give activities `location: { lat, lng }`. To prove a `feed_groups` scope exclude, give the excluded `uniqueId` group a popular activity and show it before and gone after the update. In `beforeAll` (before a `beforeEach` spy exists), wrap calls into a new group in `retry(..., { retryIf: isFeedGroupMissing })`. +- `DOCS_SYNC_WRITE` re-indents template-literal content by the region's indentation: indent it 4 extra spaces in the test. +- A file-wide `beforeEach(() => retryOnFeedGroupMissing(...))` covers flaky first reads of new groups; inside a region read results by calling `vi.spyOn` again plus `mockClear()`. +- Activity processors (`text_interest_tags`): topics aren't in the `addActivity` response; they appear on `getActivity` after ~2–10s. On a new group, about 1 activity in 4 never gets topics (likely a node with a stale group config): wait for several `getFeedGroup` reads in a row before posting, `retry(getActivity)`, and fall back to posting the same text again. The built-in `user` group has no `activity_processors`. `allowed_tags` read back sorted. +- Feeds `getOrCreate({ data })` on an existing feed ignores `data` (`created: false`): a fence that creates the same feed twice with different data is a docs bug (give the second its own id). Server-side `createFeedsBatch` requires `created_by_id` on every feed (code 4). After a hard delete task completes the same fid can be re-created (`created: true`, empty); before that `getOrCreate` answers code 16 "has been deleted". `deleteFeedsBatch` on deleted feeds fails with code 16 "can't be found". +- Fixed ids built inside a template literal (`` `feed_${index}` ``) can't be COPY-mapped: a pass-through spy that only prefixes the id field with a `uniqueId` keeps the region as written and isolated. +- `queryFeeds` full-text filters (`$q` on name, description, `created_by.name`) match single words and find new feeds within ~1s: put a run-unique word into names to scope an undefined search variable exactly; for fixed docs literals (`"Sports"`), add a public matching feed and a private one and assert inclusion / exclusion. `queryFeeds` takes no `user_id`. +- `changeFeedVisibility` (`StreamFeed.changeFeedVisibility`, no `user_id`) is async: a second change right after the first can fail with code 4 "a visibility change is already in progress for this feed" (retry in a spy), and until it applies follows behave like the old visibility. Tag-visibility activities come back `preview: true` for viewers without the tag and `false` for the owner. +- Follows: a fence that follows the same source/target pair twice is a docs bug (code 4 "... already exists in accepted state"): give the later call its own target. `StreamFeed` (`client.feeds.feed(...)`) has no `follow` / `unfollow`: a fence calling `feed.follow("group:id", {...})` is client JS SDK code (label it `JavaScript`). `UpdateFollowRequest` uses `follower_role` (unknown fields like `role` are dropped by the generated method). +- Feeds membership levels are app-wide: `uniqueId` ids on `cleanup.membershipLevels` (deleted after `cleanup.feeds`, retried while code 17 "... currently in use by feed members"). `updateMembershipLevel` returns 500 (code -1, empty message) for any level that was ever assigned to a member, also after the member is removed (2026-10-16): show an update works on a never-assigned level. Feed member roles aren't validated; `queryFeedMembers` without a filter returns accepted members only. A fence that accepts and then rejects the same invite is a docs bug ("feed member is not in pending state"). +- Feed auto-creation on `addActivity` / `upsertActivities` needs an existing user whose id equals the feed id, in any group (otherwise code 113 "Missing feed ... can't be created: user not found"): for fixed docs fids create a `uniqueId` user per feed id and use `group:`. Server-side `deleteActivity` takes no `user_id` (TS2353); `deleteActivities` takes `ids` (not `id`). Soft-deleting an already soft-deleted activity in `deleteActivities` succeeds. +- `queryActivities`: `hidden: { $eq: true }` matches nothing, use `hidden: true` (2026-10-16). For regions with app-wide filters (`{}`, `activity_type: "post"`), first `retry` a setup query scoped by `id` / `user_id` until the test's activity is indexed, then assert it's included (first for `created_at` desc). A moderation-removed activity (for `include_moderated_activities`): a `uniqueId` blocklist + config with a `remove` rule, `feed.update({ custom: { moderation_policy: key } })`, then `retry` `addActivity({ force_moderation: true })` until `activity.moderation.action === 'remove'` (~5s). +- `updateApp({ activity_metrics_config })` merges into the stored config, so `{}` changes nothing (docs say it resets). Restore an absent original with a raw `client.apiClient.sendRequest('PATCH', '/api/v2/app', undefined, undefined, { activity_metrics_config: null })` (the type has no `| null`) and `waitForAppSetting(client, 'activity_metrics_config', undefined)`. Activity `metrics` sync every ~5 minutes, so assert `trackActivityMetrics` results (`allowed`, `error`) instead; use up a limit with one large `delta` to hit the rate-limited branch. Server-side `addCommentReaction` / `queryCommentReactions` take `id` (not `comment_id`). +- Feeds bookmarks are identified by (user, activity, folder): to target a folderless bookmark in `updateBookmark` / `deleteBookmark`, leave out `folder_id`. Deleting a folder deletes its bookmarks; folder update / delete need no `user_id`. A cheap setup folder: `addBookmark({ activity_id, user_id, new_folder: { name } })`. A `next` cursor reused on a query with a different filter doesn't error but silently skips results: a docs bug. +- Server-side `updateComment` needs `user_id` (code 4 "either user or user_id must be provided when using server side auth"), although the types mark it optional: a Node.js fence without it is a docs bug. `restrict_replies: "people_i_follow"` rejects a non-follower's `addComment` with code 5. +- A COPY value can be an unquoted expression (`fixturesDir=import.meta.dirname`), so a fence that builds paths from `import.meta.dirname` can read fixtures from a tmpdir. A Node.js fence using `__dirname` together with `import` and top-level await can't run (it's an ES module): the docs fix is `import.meta.dirname`. +- Feeds URL enrichment: `https://example.com/...` has no OG data (`getOG` code 4 "could not find any opengraph data"), so COPY-map the docs URL (or whole text) to `https://getstream.io`; with `async_url_enrich_enabled` (on for the test app) attachments reach `getActivity` / `getComment` within ~1s. `getActivity` takes `{ id, comment_limit }` and returns the latest comments in `activity.comments` (no `latest_replies`); fences ported from client code (`getActivity(id, { with_recent_replies })`, `getOrCreateFeed(group, id, req)`) are docs bugs. `AddActivityRequest.expires_at` is an ISO `string`. Expired pins are left out of `queryPinnedActivities` without `include_expired_activities: true`. +- Feeds polls: a vote needs the poll in an activity first (`addActivity({ poll_id, ... })` by the poll's creator), then `client.feeds.castPollVote({ activity_id, poll_id, user_id, vote })`. `updatePollPartial` with `unset` nested inside `set` returns 200 and stores `unset` as a custom property (nothing unset): a docs bug; `unset` goes at the top level. +- Feeds collections are app-level and don't go away with their user or feed: `uniqueId` names and ids, refs (`name:id`) on `cleanup.collections`. `createCollections` on an existing ref fails with code 4 "One or more collections already exist"; `deleteCollections` on a missing ref succeeds. `StreamFeed` has no `addActivity`: a fence calling `feed.addActivity(...)` is client code, the node fix is `client.feeds.addActivity({ feeds: [fid], ..., user_id })`. An `updateFeedGroup` response omits `enable_push: false` (`push_notification` reads `{}`). +- Stories: `addActivity` accepts an `expires_at` in the past (an expired activity at once). Expired activities drop out of feed reads but `queryActivities` with an `expires_at` filter still finds them. +- To make an unread notification: `getOrCreate` the target's `notification` feed, then `addActivity` from another `uniqueId` user with `feeds: ['notification:']`; `notification_status.unread` counts it at once, and `markActivity({ mark_all_read: true, user_id })` resets it. +- Undeclared assignments in a fence (`client = new StreamClient(...)` twice) fail TS2304: the docs fix is `const client` plus a new name for the second (`clientWithTimeout`), not `let` (`no-useless-assignment`). - `notification` feeds aren't created automatically: `create_notification_activity` silently returns `notification_created: false` unless the target user's `notification` feed exists, so create it in setup. Deleting a feed that doesn't exist fails with "Feed ...: can't be found": match both "not found" and "can't be found" when cleanup ignores missing feeds. - Feed visibilities (`updateFeedVisibility`) are app-wide and not in the DRIFT snapshot. Save `getFeedVisibility({ name }).feed_visibility.grants`, restore only the roles the snippet changed (an update only touches the roles it names), and `retry` until the whole grants object equals the original. - App-level resources that the leak and drift checks don't track (e.g. retention policies): save the original list in `beforeAll`, and `cleanup.add` a restore that puts the originals back and deletes anything the file added. Check that the app is back to its original state after the run. @@ -223,6 +252,50 @@ New entries are added only after the user approves them (agents propose them in - Firebase calls validate the credentials on every request: `upsertPushProvider` and `updateApp({ firebase_config })` fail with code 4 "firebase credentials are invalid" without a real service account, even when only a template changes, so the whole test goes in `describe.skipIf(!hasFirebaseCredentials)`. On this push v3 app `getApp().app.push_notifications` legacy Firebase config is just `{ enabled: false }` and credentials are never returned: cleanup can only restore templates and the enabled flag. - Export tasks (`exportChannels`, `exportUsers`, `feeds.exportFeedUserData`; also `feeds.deleteFeedUserData`) finish within seconds: `waitForTask` is enough, and `result.url` downloads with plain `fetch` (no auth), so assert the export's contents (v2: one JSON object per line). - `client.moderation.flag`: a repeat flag by the same user on the same entity raises `flags_count`, but the review item's `flags` keep only the first one (later `reason` / `custom` are dropped). A fence that flags one placeholder id several times from one user with different reasons is a docs bug: give each call its own entity id. Check flags by `retry`ing `client.moderation.queryReviewQueue({ filter: { entity_id } })` until it returns one item (about a second), then assert `items[0].flags[i]`. +- Moderation configs: use `uniqueId` keys (a channel-level chat policy is `chat:messaging:`) and `cleanup.add(() => client.moderation.deleteConfig({ key }))`. COPY-map app-wide keys (`"feeds"`, `"chat:messaging"`) and blocklist names (`profanity_en` doesn't exist on the test app, only `profanity` and `url_detection_v1`) to test-owned ones. +- Right after `createBlockList`, `upsertConfig` referencing it can fail with code 4 "Blocklist not found" for ~5s: retry on that message (in a spy when it happens inside a region). A new policy can take a moment to apply: before asserting a block, `retry` a setup `check` / message until it is blocked. +- `ai_text_config` rule labels are validated (code 4 "invalid ai_text config rule label: X"). Accepted (2026-10-16): SPAM, SEXUAL_HARASSMENT, INSULT, HATRED, THREAT, MORAL_HARASSMENT, TROLLING, SCAM, SELF_HARM, DOXXING, PII, PLATFORM_BYPASS, BODY_SHAMING; `HARASSMENT`, `TOXIC`, `HATE_SPEECH`, `VIOLENCE` are rejected. +- Feeds `addActivity` with `force_moderation` returns `moderation: { action, original_text, blocklist_matched, blocklists_matched }` (no `status` / `recommended_action`). A banned user's `addActivity` fails with code 17 "... Your account is currently banned." +- Call moderation rules (`upsertModerationRule` with `rule_type: "call"`): `keyframe_rule_params` and `closed_caption_rule_params` require `time_window` (15s, 30s, 1m, 5m, 30m, 1h, 24h, 7d, 30d), and `keyframe_rule_params.min_confidence` must be 0.0–1.0. Rules have no client-chosen `id` (TS2353; the API assigns a UUID): give them `uniqueId` names and delete them by the id from the upsert response (spy on `upsertModerationRule`). +- `moderation.check` image checks return `status: "partial"` plus a `task_id` (async analysis) while a call rule with a keyframe condition exists on the app, `"complete"` otherwise: accept both. +- `CheckRequest.content_published_at` is a `Date`: a fence passing `new Date().toISOString()` fails TS2322, and the docs fix is `new Date()`. +- `upsertModerationRule` requires `rule_type` (user, content, call, flood); its `config_keys` aren't validated (a nonexistent `chat:messaging:` is accepted), and condition harm labels (`text_content_params.harm_labels`) aren't validated either, unlike `ai_text_config` labels. `getModerationRule` on a missing id rejects with code -1 and an empty message: prove a delete with `queryModerationRules({ filter: { id } })` returning `[]`. +- To prove a moderation config applies without a channel: `client.moderation.check({ entity_type: 'stream:chat:v1:message', entity_id: uniqueId(...), entity_creator_id, config_key, moderation_payload: { texts: [...] } })` against the test-owned key, asserting `recommended_action` (`retry` the first check). `deleteBlockList` succeeds while a config still references the blocklist. +- Moderation evaluation sets (`createPolicyTestSet`) get server-assigned ids and unique-per-app names (code 4 "... already exists"): use `uniqueId` names (the leak check matches them) and delete them by id. Runs finish within about a second: `retry` `getPolicyTestRun` until `status` is terminal. Seeded sets sample app-wide moderated content whatever `config_key` is passed. +- Team moderation: `upsertConfig({ team })` needs multi-tenancy on (code 4 "Team moderation configuration is not supported in single tenant mode"); `getConfig` / `deleteConfig` need the `team` too (without it: code 16). Team blocklists (`createBlockList({ team })`, works with multi-tenancy off) need `team` on get / update / delete, and `listBlockLists({ team })` is off on the test app (code 4 "Multi-tenant blocklist is not enabled for this app"). The leak check can't see either: register deletes with the team inside the multi-tenancy `describe`. Review queue `teams` filters match items whose entity creator is in the team. +- `queryModerationConfigs({ limit })` returns no `next` even when more configs exist: don't rely on its pagination cursor. +- `flood_config` works on `moderation.check`: N identical checks from one user (threshold N) give `keep` ×(N-1), then `flag`. +- `customCheck` responses have `item.flags: []`; the flags reach the review queue item about a second later (`waitForReviewItem(client, entityId, { flags })`). +- Server-side `client.queryBannedUsers` takes `{ payload: { filter_conditions, sort, limit, exclude_expired_bans } }`: top-level fields are a type error and never sent. `exclude_expired_bans` can be proven with a `timeout: 1` (minute) ban and a `retry` until it has expired (give that test ~150s). +- Ban and mute `expires` is exactly `created_at` + timeout, and the API clock can be a few ms ahead of the local one: compare `expires - created_at`, not against `Date.now()`. +- `client.moderation.bypass` takes `{ target_user_id, enabled }` (read back as `bypass_moderation` on the user). Block users with `client.blockUsers({ blocked_user_id, user_id })` and check `getBlockedUsers({ user_id })`. +- Moderation custom views (`createQueue`) have server-assigned ids and are capped at 10 per app: give them `uniqueId` names and delete them by id (an already deleted view fails with code 16; `deleteQueue` returns `queue: null`). `listQueues({ user_id })` re-creates a deleted user, so call it before the users are deleted. For a `queryReviewQueue({ filter: { reviewed: false } })` snippet, add a negative control: flag a newer item, `submitAction({ action_type: 'mark_reviewed', item_id, user_id })` it and assert it is absent. +- `submitAction` ban with `item_id`: the channel comes from the flagged message and only `ban: { channel_ban_only: true }` makes it a channel ban; unknown ban fields (e.g. `channel_cids`) are silently ignored and give an app-wide ban. Check bans with `queryBannedUsers` (`bans[i].channel?.cid`). To prove `ban_from_future_channels`, create a new channel by the original channel's creator with the target as member and check `members[].banned` from `getOrCreate({ state: true })`. `getReviewQueueItem(...).item.latest_moderator_action` reads back every `submitAction` (`custom:` for custom actions). Use a fresh `uniqueId` creator per flagged item when tests ban users. +- Appeals: content can only be appealed after a moderator action (e.g. `delete_message`) or a policy block, not on an unreviewed flag (code 4 "... flagged but not yet reviewed by a moderator"); ban appeals need an active ban (app-wide and channel bans are appealed separately, with `channel_cid`). For blocked content to `unblock`, use a channel-level blocklist policy plus `force_moderation: true` (`submitAction` `block` is rejected: "invalid action type: block"). Accepting via `unban` / `unblock` reads back `status: 'accepted'` with the `decision_reason`. +- `queryModerationLogs` rejects unknown filter fields (code 4 "unrecognized field"): allowed are id, type, created_at, date_range, target_user_id, entity_id, reporter_id, reporter_type, moderation_category, ai_providers, review_queue_item_id, reason, resource_type, label, config_key. +- To probe an endpoint the node SDK doesn't have: `client.moderation.apiClient.sendRequest(method, path, undefined, query, body)`. +- `upsertConfig` replaces the whole config: every engine config the request leaves out is reset to null. A fence that upserts an existing key with a partial config wipes the rest of the policy. `rule_builder_config` (only `async`, `rules`) is neither stored nor returned; moderation rules apply through their `config_keys` without an enable step. +- A fence using `require(...)` gets `any` and fails eslint (`no-require-imports`, `no-unsafe-*`): the docs fix is the equivalent ESM `import`. +- Video: `call.create` / `getOrCreate` on an existing call ignores `data.members` and `data.custom` (`created: false`, call unchanged): a fence that creates the same call twice with different data is a docs bug, so give the second create its own call id. In `GetOrCreateCallResponse.members` a member added without `role` has `role` undefined (`queryMembers` after `updateCallMembers` returns `'user'`). +- A client fence with empty credentials (`const apiKey = "";`) can be a module-level region with `COPY: streamApiKey=""` mapped to `process.env.STREAM_API_KEY`. The SDK's default timeout (3000) can be asserted with `vi.spyOn(AbortSignal, 'timeout')`. +- Video `queryCalls`: a `next` / `prev` request must repeat the original `sort` (without it: code 4 "cannot specify sort and next/prev at the same time", a misleading message). Sorting by `starts_at` returns an empty next page when the values are null. `filter_conditions: { backstage: { $eq: false } }` only matches calls that went live: create a `livestream` call and `goLive()` it (`default` has backstage off: "backstage is not enabled for call types default"). +- `StreamCall.queryMembers` takes the request directly (no `payload` wrapper, unlike `queryUsers` / `queryBannedUsers`). `StreamCall` has no `settings`: read them with `call.get()`. `UpdateCallRequest` has `settings_override` at the top level (no `data`). +- Video call type grants for an unknown role fail with code 4 `cannot use unknown role "x" in permission grants`; a just-created role can be unknown briefly (retry on "unknown role"). `updateCallType({ grants: {} })` leaves the grants unchanged. The node SDK exports `OwnCapability` (docs' `VideoOwnCapability` is a docs bug). Built-in call types: `audio_room`, `default`, `development`, `livestream`. +- Video `goLive({ start_hls, start_recording, start_closed_caption, start_transcription, ... })` on a call with no participants succeeds (storage names aren't validated) but starts nothing (`recording` / `transcribing` / `captioning` false, `egress.hls` null): assert `backstage`, not the processes. `updateCallType({ grants: { : [...] } })` replaces that role's list; a new call type has no `host` grants. +- `StreamCall.createSRTCredentials(userId)` is local and needs `call.data` from an awaited `get()` / `getOrCreate()` ("Object is not initialized"). Parse the address with `new URL(address)`: the last `streamid` segment is the user's JWT and `passphrase` its signature (don't compare with a fresh token: `iat` can differ). +- Video egress (`startHLSBroadcasting`, `startRTMPBroadcasts`, recording, ...) on a call nobody joined fails with code 4 "cannot broadcast inactive call", and the stop calls then fail too ("call egress is not running" code 4; "no active rtmp broadcasts ..." code 4 / 16): those fences are ⛔ BLOCKED with the exact error. A fresh call's `egress` is `{ broadcasting: false, hls: null, rtmps: [] }`. +- **Video call sessions without WebRTC**: `joinCallAsUser(call, userId)` (`helpers/video.ts`) joins over REST (`POST /video/call/{type}/{id}/join` with a user token) and starts a session. After it, `startRecording` / `startTranscription` / `startClosedCaptions` / `startHLSBroadcasting` / `startFrameRecording` and their stops work (2026-10-16), so egress fences aren't BLOCKED: join a `uniqueId` member first. `postAsVideoUser(userId, path, body)` calls other client-side video endpoints (e.g. `feedback`: needs a joined session, `sdk_version` ≥ 2 chars; server-side `collectUserFeedback` fails with "user_id is missing"). Feedback goes away with the call or user. `getActiveCallsStatus` is limited to 6 requests per minute (`retryOnRateLimit` spy). +- Video egress state lags behind the start: `recording` turns true ~6–9s after `startRecording`, `transcribing` / `captioning` after a few seconds; `egress._recording.status` / `frame_recording.status` are `starting` at once. Read state with `retry` (inside a spy on the start method if the fence stops right after). Register a `cleanup.add` stop (ignoring errors) for every egress a test starts. `startRTMPBroadcasts` to a placeholder target answers 201 but `egress.rtmps` stays `[]`; transcription to a placeholder S3 storage is accepted but `transcribing` never turns true. Only the creator (or a host) can REST-join a `livestream` call in backstage (a member gets code 17 JoinBackstage). +- To test `deleteRecording`: record a composite for ~10s on a joined call, `retry` until `listRecordings` returns it (~15s after the stop), and COPY-map `session_id` / `filename`. +- A custom role granted in a call type can't be deleted until that type is gone (code 4 "... present in permission grants of scopes [video:]"), and `cleanup.callTypes` runs after `cleanup.add` steps: delete the type inside the role's cleanup step. To stack a retry on a shared spy helper, wrap `spy.getMockImplementation()` (spying the same method again recurses). +- Video participant stats (`queryCallParticipantSessions`, `getCallParticipantSessionMetrics`, `queryCallStats` reports) need real SFU participants: a REST join leaves `call.session.participants` empty, and the endpoints answer code 16 "... not found" / "no stats available" indefinitely. Run single-call fences against a joined session with a spy that records the 404 (the request is still validated first: time window ≤ 30 min, `limit` ≤ 100, cursor, session id); `it.skip('BLOCKED: ...')` loops that page through results. +- Video `call.delete()` (soft) makes the call unreachable: a later hard delete fails with code 16 "Can't find call with id ..." (`cleanup.calls` ignores it) and the leak check can't list it, so a soft-deleted call stays on the app. `call.blockUser` / `kickUser` / `muteUsers` / `updateUserPermissions` work on a call with a REST-joined member. +- Video recording (`startRecording`) on a call nobody joined fails with code 4 "there is no active session"; stops fail with "call egress is not running" (composite) or `recording "call_raw_recorder:" is not running` (individual / raw). `StreamCall.startRecording` / `stopRecording` take `{ recording_type, ...request }`, not a positional string. `recording` settings (call `settings_override` or call type) with `mode` `available` / `auto-on` and `audio_only` false need `quality` (code 4 "recording quality is required when audio_only is false and recording is enabled"), even when the call already has one. +- External storages are app-wide (max 10 per app; the built-in `stream-s3` is always listed): `uniqueId` names, `cleanup.add(() => deleteExternalStorageIfExists(client, name))` (`helpers/server.ts`: a delete right after the create can fail with code 4 "... does not exist" while the storage is still listed). Placeholder S3 / ABS credentials are accepted on create and only fail `checkExternalStorage` (code 4 "InvalidAccessKeyId ..."); GCS credentials are validated on create (code 4 "invalid gcs credentials"). `startFrameRecording` / `startTranscription` on a call nobody joined fail with code 4 "there is no active session"; `deleteRecording` of an unknown file fails with code 16 "recording doesn't exist". +- The node SDK has no `Video...Enum` objects (TS2304): the docs fix is the string literal (`"1080p"`, `"spotlight"`). `updateCallType` takes `{ name, ...request }`, not positional args. +- COPY on a name the fence declares (`const callId = "my-call"`) drops that declaration on write-back: COPY a different name and keep `const callId = myCallId;` in the region. +- `moderation.ban` `timeout` is in minutes (prove it with `expires - created_at`). `ai_image_config.ocr_rules` take text harm labels (SPAM, INSULT, HATRED, SEXUAL_HARASSMENT, THREAT), not image labels (code 4 "invalid ai_image ocr rule label: X"). Blocklist words are at most 40 characters: don't build them from a `uniqueId`. `getApp().app.event_hooks` order isn't stable: match hooks by a field, not by index. +- A bare expression statement in a fence (`client.moderation;`) fails `no-unused-expressions`: the docs fix is to assign it. `${e}` on an unknown catch value fails `restrict-template-expressions`: the docs fix is `${String(e)}`. ### Server-side vs client-side diff --git a/__tests__/docs-snippets/README.md b/__tests__/docs-snippets/README.md index 6ce1c11..1764592 100644 --- a/__tests__/docs-snippets/README.md +++ b/__tests__/docs-snippets/README.md @@ -1,9 +1,15 @@ # Docs snippet tests -Typed tests that run the server-side Node.js snippets of the chat docs (the pages of the Node sidebar, -`getstream.io/content/docs/_sidebars/[chat][node].json`) and of the platform docs (the pages of -`getstream.io/content/docs/_sidebars/[platform][default].json`) against a real Stream app. -Only fences labelled `Node.js` are in scope (docs fences must always have a label): they are fixed when they are +Typed tests that run the server-side Node.js snippets of the Stream docs against a real Stream app: the pages of +these sidebars in `getstream.io/content/docs/_sidebars/` (`SIDEBAR_FILES` in `sync/parse.ts`): + +- chat: `[chat][node].json` +- activity feeds (v3): `[activity-feeds][node][v3].json` +- moderation: `[moderation][node].json` +- video: `[video][api].json` (the server-side API docs) +- platform: `[platform][default].json` + +Only fences labelled `Node.js` are in scope (a fence label, or the `label` of the enclosing `` on the video pages) (docs fences must always have a label): they are fixed when they are wrong. `JavaScript`, `React` and `React Native` fences (client-side code) and `Node.js` fences inside a `` block (reference implementations without the SDK) are out of scope and are neither tested nor changed. @@ -38,6 +44,9 @@ check that notifications or webhook events arrive. ``` pages/chat//.test.ts # chat Node.js snippets, run with the server client +pages/activity-feeds/v3-latest//.test.ts # feeds v3 Node.js snippets +pages/moderation//.test.ts # moderation Node.js snippets +pages/video/api//.test.ts # video API Node.js snippets pages/platform//.test.ts # platform Node.js snippets helpers/ # server client, unique ids, cleanup registry, server-side helpers, # waits, leak check, app snapshot, push credentials @@ -52,7 +61,9 @@ AGENTS.md # test conventions and API quirks, r The path mirrors the docs path (relative to `content/docs`) without `/_default`, e.g. `chat/_default/04-messages/01-send_message.md` maps to `pages/chat/04-messages/01-send_message.test.ts`, -`chat/node/01-quick_start/...` to `pages/chat/node/01-quick_start/...` and +`chat/node/01-quick_start/...` to `pages/chat/node/01-quick_start/...`, +`activity-feeds/v3-latest/_default/05-activities/01-activities.md` to `pages/activity-feeds/v3-latest/05-activities/01-activities.test.ts`, +`video/api/01-basics/03-calls.md` to `pages/video/api/01-basics/03-calls.test.ts` and `platform/02-identity-and-access/02-users.md` to `pages/platform/02-identity-and-access/02-users.test.ts`. ## Conventions @@ -74,14 +85,14 @@ await channel.getOrCreate({ data: { created_by_id: creatorId } }); ``` - **Fence key**: - - `docs` is the page path relative to `getstream.io/content/docs/` (`chat/...` or `platform/...`). + - `docs` is the page path relative to `getstream.io/content/docs/` (`chat/...`, `activity-feeds/...`, `moderation/...`, `video/...` or `platform/...`). - `heading` is the text of the nearest heading above the fence. - - `tab` is the fence `label` (or `unlabelled`). + - `tab` is the fence `label`, else the `label` of the enclosing `` (or `unlabelled`). - `index` is 1-based among fences with the same heading text and tab (default 1). - **COPY**: `name="literal"` pairs, applied to the region before comparing (identifier `name` becomes the docs literal). Don't use COPY variables as object keys or shorthand properties. - **Comparison**: both sides are formatted with the same prettier options, so quote style and line wrapping don't matter, but any code or comment change does. -- **Coverage** (checked first): every `Node.js` fence (`js`/`javascript`/`ts`/`typescript`) on the pages of the chat Node sidebar and the platform sidebar (`content/docs/_sidebars/[chat][node].json`, `[platform][default].json`), whether or not the page has tests yet, must have a region (inside `it.skip` if it can't run). `JavaScript` / `React` / `React Native` fences and comment-only fences are ignored. +- **Coverage** (checked first): every `Node.js` fence (`js`/`javascript`/`ts`/`typescript`) on the pages of the tested sidebars (see the top of this file; `$fragment` entries are resolved from `_sidebars/fragments/`), whether or not the page has tests yet, must have a region (inside `it.skip` if it can't run). `JavaScript` / `React` / `React Native` fences and comment-only fences are ignored. - **Labels**: every JS fence on those pages must be labelled `JavaScript`, `React`, `React Native` or `Node.js`; unlabelled or otherwise labelled JS fences fail. -- **Excluded pages**: `EXCLUDED_PAGES` in `sync/parse.ts` lists sidebar pages that are deliberately not validated, with a reason. Both checks skip them. +- **Excluded pages and fences**: `EXCLUDED_PAGES` in `sync/parse.ts` lists sidebar pages that are deliberately not validated, with a reason. Both checks skip them. `EXCLUDED_FENCES` does the same for single `Node.js` fences (keyed by fence id) that hold another SDK's code, e.g. Feeds v2 `getstream` client code on the moderation Feeds v2 quick start: not ours to test or update. - **`DOCS_SYNC_WRITE= yarn test:docs:sync`** writes the regions of that page into the docs fences (docs prettier style). Use it to apply a fix to the docs. `DOCS_SYNC_WRITE=1` writes all pages. - **`DOCS_DIR`** points at another docs checkout (default: `../getstream.io/content/docs` next to this repo). The check is skipped if the directory doesn't exist. diff --git a/__tests__/docs-snippets/docs-snippets-todo.md b/__tests__/docs-snippets/docs-snippets-todo.md index e20a45c..bcf60d2 100644 --- a/__tests__/docs-snippets/docs-snippets-todo.md +++ b/__tests__/docs-snippets/docs-snippets-todo.md @@ -8,6 +8,18 @@ Open issues found while testing the docs snippets, in two lists: [API spec / gen - **05-features/06-campaign_api** (API spec): `updateCampaign` and `stopCampaign` are typed as returning `CampaignResponse`, but the API returns `{ campaign: CampaignResponse, ... }` (like `StartCampaignResponse`), so `response.campaign` is a TS2339 error and `response.name` etc. are undefined at runtime. The response schemas of `PUT /campaigns/{id}` and `POST /campaigns/{id}/stop`. Reported: [Slack thread](https://getstream.slack.com/archives/C07AL9Q0T2T/p1791403182844109). - **04-messages/08-unread_reminders** (API spec, to check): `CreateChannelTypeRequest` has no `reminders` field, while `UpdateChannelTypeRequest` has it, so a channel type can't be created with reminders on in one call from typed code. Check whether the spec (`CreateChannelType`) lacks it or the API doesn't accept it. Reported: [Slack thread](https://getstream.slack.com/archives/C07AL9Q0T2T/p1791403182844109). - **platform/02-identity-and-access/01-authentication** (node SDK generator): `UpdateAppRequest.revoke_tokens_issued_before` is typed `Date` without `| null`, so the docs' `updateApp({ revoke_tokens_issued_before: null })` ("Undoing the revoke", Node.js #2) fails TS2322 and `yarn types:docs` reports it. Same root cause as the `grants: null` entry above (the generator drops `nullable`); check that the spec marks the field nullable. Not run against the API (see Blockers detail). `UpdateUserPartialRequest.set` is `Record`, so the user-level `set: { revoke_tokens_issued_before: null }` typechecks. Reported (as the generator `nullable` issue): [Slack thread](https://getstream.slack.com/archives/C07AL9Q0T2T/p1791403182844109). +- **moderation/02-quick-start/08-quick-start-feeds-v3-moderation** (API spec, to check): `AWSRekognitionRule.min_confidence` is required in the generated types (TS2741 without it), while the API accepts an `ai_image_config` rule without it (stored as `0`) and the docs treat it as optional ("Default is 50" on the call moderation page). The Node.js fence now passes `min_confidence: 50` explicitly; check whether the spec should mark it optional (and what the default really is). +- **moderation/02-quick-start/03-quick-start-feeds-moderation** (API spec): `DELETE /api/v2/moderation/feeds_moderation_template` (`v2DeleteTemplate`) has no request in the spec, so the generated method takes no arguments and sends no body. The API requires `name` in the DELETE body (code 4 `V2DeleteTemplate failed with error: "name is a required field"`; `?name=` doesn't work either), as in chat's `DeleteModerationTemplateRequest` (`Name` is `validate:"required"`). The docs' `v2DeleteTemplate({ name: "mod_tmpl_1" })`("Delete Template") fails TS2554, so a template can't be deleted from typed node SDK code (tests use`deleteFeedModerationTemplate`in`helpers/server.ts`). Other SDKs (Go `V2DeleteTemplateRequest{Name}`, Python) take a name. +- **moderation/04-configuration/05-evaluations** (API spec): `GET /api/v2/moderation/policy_tests/sets` (`listPolicyTestSets`) declares no query params, so the generated method takes no arguments and the docs' `listPolicyTestSets({ limit: 50 })` ("List evaluation sets") fails TS2554 (`yarn types:docs` reports it). The API reads `limit` / `offset` from the query string (chat `lib/moderation/controller/policytest.go`, `ListPolicyTestSets`, whose `OpenAPIInfo` has no Request); a raw `GET ?limit=1` returns one set. The Python and Go tabs pass `limit`. +- **moderation/05-content-moderation/01-check** (API spec): `POST /api/v2/moderation/analyze` (`client.moderation.analyze`) only accepts `multipart/form-data` with a JSON `payload` part (Blob, `application/json`; optional `image: