diff --git a/docs/guide/features.md b/docs/guide/features.md
index 0f961b6..69b486c 100644
--- a/docs/guide/features.md
+++ b/docs/guide/features.md
@@ -12,9 +12,9 @@ Trotsky is currently limited to the following features:
**StepActorLikes** | :white_check_mark: | Get an actor's likes. | ```Trotsky.init(agent).actor('bsky.app').likes().each()```
**StepActorLists** | :white_check_mark: | Get an actor's lists. | ```Trotsky.init(agent).actor('bsky.app').lists().each()```
**StepActorMute** | :white_check_mark: | Mute an actor. | ```Trotsky.init(agent).actor('bsky.app').mute()```
- **StepActorPosts** | :white_check_mark: | Get an actor's posts | ```Trotsky.init(agent).actor('bsky.app').posts().each()```
- **StepActors** | :white_check_mark: | Get a list of actors by their DIDs or handles. | ```Trotsky.init(agent).actors(['bsky.app', 'trotsky.pirhoo.com']).each()```
- **StepActorStarterPacks** | :x: | Get an actor starter packs. |
+ **StepActorPosts** | :white_check_mark: | Get an actor's posts | ```Trotsky.init(agent).actor('bsky.app').posts().each()```
+ **StepActors** | :white_check_mark: | Get a list of actors by their DIDs or handles. | ```Trotsky.init(agent).actors(['bsky.app', 'trotsky.pirhoo.com']).each()```
+ **StepActorStarterPacks** | :white_check_mark: | Get an actor's starter packs. | ```Trotsky.init(agent).actor('bsky.app').starterPacks().each()```
**StepActorStreamPosts** | :test_tube: | Stream an actor's posts. | ```Trotsky.init(agent).actor('bsky.app').streamPosts().each()```
**StepActorUnblock** | :white_check_mark: | Unblock an actor. | ```Trotsky.init(agent).actor('bsky.app').unblock()```
**StepActorUnfollow** | :white_check_mark: | Unfollow an actor. | ```Trotsky.init(agent).actor('bsky.app').unfollow()```
@@ -29,10 +29,10 @@ Trotsky is currently limited to the following features:
**StepPostReply** | :white_check_mark: | Reply to a post. | ```Trotsky.init(agent).post("at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6oveex3ii2l").reply({ text: "Well done!" })```
**StepPostRepost** | :white_check_mark: | Repost a post. | ```Trotsky.init(agent).post("at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6oveex3ii2l").repost()```
**StepPosts** | :white_check_mark: | Get a list of post by their URIs. | ```Trotsky.init(agent).posts(["at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6oveex3ii2l"]).each()```
- **StepSearchPosts** | :white_check_mark: | Search posts. | ```Trotsky.init(agent).searchPosts({ q: "Mapo Tofu" }).each()```
- **StepSearchStarterPacks** | :x: | Search starter packs. |
- **StepStarterPack** | :x: | Get a start pack by its URI. |
- **StepStarterPacks** | :x: | Get a list of starter packs by their URIs. |
+ **StepSearchPosts** | :white_check_mark: | Search posts. | ```Trotsky.init(agent).searchPosts({ q: "Mapo Tofu" }).each()```
+ **StepSearchStarterPacks** | :white_check_mark: | Search starter packs. | ```Trotsky.init(agent).searchStarterPacks({ q: "tech" }).each()```
+ **StepStarterPack** | :white_check_mark: | Get a starter pack by its URI. | ```Trotsky.init(agent).starterPack("at://did:plc:example/app.bsky.graph.starterpack/packid")```
+ **StepStarterPacks** | :white_check_mark: | Get a list of starter packs by their URIs. | ```Trotsky.init(agent).starterPacks([uri1, uri2]).each()```
**StepStreamPosts** | :test_tube: | Use the firehose to stream posts. | ```Trotsky.init(agent).streamPosts().each()```
**StepTimeline** | :x: | Get the timeline. |
diff --git a/lib/core/StepActorStarterPacks.ts b/lib/core/StepActorStarterPacks.ts
new file mode 100644
index 0000000..efc594d
--- /dev/null
+++ b/lib/core/StepActorStarterPacks.ts
@@ -0,0 +1,90 @@
+import type { AppBskyGraphGetActorStarterPacks } from "@atproto/api"
+
+import { StepStarterPacks, type StepStarterPacksOutput } from "./StepStarterPacks"
+import type { StepActor } from "./StepActor"
+import type { StepActorOutput } from "./StepActor"
+
+/**
+ * Type representing the output of the starter packs retrieved by {@link StepActorStarterPacks}.
+ * @public
+ */
+export type StepActorStarterPacksOutput = StepStarterPacksOutput
+
+/**
+ * Type representing the query parameters for retrieving actor starter packs.
+ * @public
+ */
+export type StepActorStarterPacksQueryParams = AppBskyGraphGetActorStarterPacks.QueryParams
+
+/**
+ * Type representing the cursor for paginated queries.
+ * @public
+ */
+export type StepActorStarterPacksQueryParamsCursor = StepActorStarterPacksQueryParams["cursor"] | undefined
+
+/**
+ * Represents a step for retrieving an actor's starter packs using the Bluesky API.
+ * Supports paginated retrieval of starter packs.
+ *
+ * @typeParam P - Type of the parent step, extending {@link StepActor}.
+ * @typeParam C - Type of the context object, extending {@link StepActorOutput}.
+ * @typeParam O - Type of the output object, extending {@link StepActorStarterPacksOutput}.
+ *
+ * @example
+ * Get an actor's starter packs:
+ * ```ts
+ * await Trotsky.init(agent)
+ * .actor("alice.bsky.social")
+ * .starterPacks()
+ * .each()
+ * .tap((step) => {
+ * console.log(`Pack: ${step.context.uri}`)
+ * console.log(`Creator: @${step.context.creator.handle}`)
+ * console.log(`Members: ${step.context.listItemCount || 0}`)
+ * })
+ * .run()
+ * ```
+ *
+ * @example
+ * Follow members from an actor's popular starter packs:
+ * ```ts
+ * await Trotsky.init(agent)
+ * .actor("bob.bsky.social")
+ * .starterPacks()
+ * .each()
+ * .when((step) => (step?.context?.joinedAllTimeCount || 0) > 50)
+ * .tap((step) => {
+ * console.log(`Popular pack: ${step.context.uri}`)
+ * })
+ * .run()
+ * ```
+ *
+ * @public
+ */
+export class StepActorStarterPacks
extends StepStarterPacks
{
+
+ /**
+ * Applies pagination to retrieve starter packs and sets the output.
+ * Fetches paginated results using the agent and appends them to the output.
+ */
+ async applyPagination () {
+ this.output = await this.paginate("starterPacks", (cursor) => {
+ return this.agent.app.bsky.graph.getActorStarterPacks(this.queryParams(cursor))
+ })
+ }
+
+ /**
+ * Generates query parameters for retrieving starter packs, including the optional cursor.
+ * @param cursor - The cursor for paginated queries.
+ * @returns The query parameters for retrieving starter packs.
+ * @throws
+ * Error if no context is found.
+ */
+ queryParams (cursor: StepActorStarterPacksQueryParamsCursor): StepActorStarterPacksQueryParams {
+ if (!this.context) {
+ throw new Error("No context found for StepActorStarterPacks")
+ }
+
+ return { "actor": this.context.did, cursor }
+ }
+}
diff --git a/lib/core/StepActorStreamPosts.ts b/lib/core/StepActorStreamPosts.ts
index 85b64a1..bbe211e 100644
--- a/lib/core/StepActorStreamPosts.ts
+++ b/lib/core/StepActorStreamPosts.ts
@@ -1,4 +1,4 @@
-import { type PostRecord } from "@atproto/api"
+import { type AppBskyFeedPostRecord } from "@atproto/api"
import { StepStreamPosts, type StepActorOutput } from "../trotsky"
import { buildEventEmitter, JetstreamMessageCommit } from "./utils/jetstream"
@@ -7,7 +7,7 @@ import { buildEventEmitter, JetstreamMessageCommit } from "./utils/jetstream"
* Typically represents the streamed output of a single post event.
* @public
*/
-export type StepActorStreamPostsOutput = JetstreamMessageCommit & { "record": Partial }
+export type StepActorStreamPostsOutput = JetstreamMessageCommit & { "record": Partial }
/**
* @experimental
diff --git a/lib/core/StepSearchStarterPacks.ts b/lib/core/StepSearchStarterPacks.ts
new file mode 100644
index 0000000..13ea457
--- /dev/null
+++ b/lib/core/StepSearchStarterPacks.ts
@@ -0,0 +1,120 @@
+import type { AppBskyGraphSearchStarterPacks, AtpAgent } from "@atproto/api"
+
+import { StepStarterPacks } from "../trotsky"
+
+/**
+ * Represents the output of a search starter packs step, consisting of an array of starter packs.
+ * @public
+ */
+export type StepSearchStarterPacksOutput = AppBskyGraphSearchStarterPacks.OutputSchema["starterPacks"]
+
+/**
+ * Represents the query parameters for searching starter packs.
+ * @public
+ */
+export type StepSearchStarterPacksQueryParams = AppBskyGraphSearchStarterPacks.QueryParams
+
+/**
+ * Represents the cursor for paginating through search starter pack results.
+ * @public
+ */
+export type StepSearchStarterPacksQueryParamsCursor = StepSearchStarterPacksQueryParams["cursor"] | undefined
+
+/**
+ * Represents a step for searching starter packs on Bluesky, with support for pagination.
+ *
+ * @remarks
+ * Note: The `searchStarterPacks` API endpoint may not be available on all Bluesky servers.
+ * As of November 2025, this API is available in test environments but not yet deployed to
+ * the main Bluesky network. When the API is not available, it will throw an XRPCNotSupported error.
+ *
+ * @typeParam P - The parent type of this step.
+ * @typeParam C - The child context type, defaulting to `null`.
+ * @typeParam O - The output type, defaulting to {@link StepSearchStarterPacksOutput}.
+ *
+ * @example
+ * Search for starter packs and display them:
+ * ```ts
+ * await Trotsky.init(agent)
+ * .searchStarterPacks({ q: "typescript" })
+ * .take(10)
+ * .each()
+ * .tap((step) => {
+ * console.log(`Pack URI: ${step.context.uri}`)
+ * console.log(`Creator: @${step.context.creator.handle}`)
+ * console.log(`Members: ${step.context.listItemCount || 0}`)
+ * })
+ * .run()
+ * ```
+ *
+ * @example
+ * Filter starter packs by join count:
+ * ```ts
+ * await Trotsky.init(agent)
+ * .searchStarterPacks({ q: "tech" })
+ * .take(20)
+ * .each()
+ * .when((step) => (step?.context?.joinedAllTimeCount || 0) > 100)
+ * .tap((step) => {
+ * console.log(`Popular pack: ${step.context.uri}`)
+ * })
+ * .run()
+ * ```
+ *
+ * @public
+ */
+export class StepSearchStarterPacks extends StepStarterPacks
{
+
+ /**
+ * The initial query parameters for the search.
+ */
+ _queryParams: StepSearchStarterPacksQueryParams
+
+ /**
+ * Initializes the StepSearchStarterPacks instance with the given agent, parent, and query parameters.
+ *
+ * @param agent - The AT protocol agent used for API calls.
+ * @param parent - The parent step in the chain.
+ * @param queryParams - The initial query parameters for the search.
+ */
+ constructor (agent: AtpAgent, parent: P, queryParams: StepSearchStarterPacksQueryParams) {
+ super(agent, parent)
+ this._queryParams = queryParams
+ }
+
+ /**
+ * Clones the current step and returns a new instance with the same parameters.
+ * @param rest - Additional parameters to pass to the cloned step. This is useful for child class overriding the clone.
+ * @returns A new {@link StepSearchStarterPacks} instance.
+ */
+ override clone (...rest: unknown[]) {
+ const step = super.clone(...rest)
+ step._queryParams = this._queryParams
+ return step
+ }
+
+ /**
+ * Applies the pagination logic to retrieve starter packs based on the search parameters.
+ * Results are stored in the `output` property.
+ *
+ * @override
+ */
+ async applyPagination () {
+ this.output = await this.paginate("starterPacks", (cursor) => {
+ return this
+ .agent
+ .app.bsky.graph
+ .searchStarterPacks(this.queryParams(cursor))
+ })
+ }
+
+ /**
+ * Constructs the query parameters for the current pagination step.
+ *
+ * @param cursor - The cursor indicating the current position in the paginated results.
+ * @returns The query parameters for the search.
+ */
+ queryParams (cursor: StepSearchStarterPacksQueryParamsCursor): StepSearchStarterPacksQueryParams {
+ return { ...this._queryParams, cursor }
+ }
+}
diff --git a/lib/core/StepStarterPack.ts b/lib/core/StepStarterPack.ts
new file mode 100644
index 0000000..0038b0b
--- /dev/null
+++ b/lib/core/StepStarterPack.ts
@@ -0,0 +1,111 @@
+import type { AtUri, AtpAgent, AppBskyGraphGetStarterPack, AppBskyGraphDefs } from "@atproto/api"
+
+import { Step, type StepBuilder } from "../trotsky"
+import type { Resolvable } from "./utils/resolvable"
+import { resolveValue } from "./utils/resolvable"
+
+/**
+ * Defines the query parameters for retrieving a starter pack.
+ * @public
+ */
+export type StepStarterPackQueryParams = AppBskyGraphGetStarterPack.QueryParams
+
+/**
+ * Represents a starter pack's URI, which can be a string or an {@link AtUri}.
+ * @public
+ */
+export type StepStarterPackUri = string | AtUri
+
+/**
+ * Represents the output of a retrieved starter pack, including its URI, CID, record data, creator, and associated lists/feeds.
+ * @public
+ */
+export type StepStarterPackOutput = AppBskyGraphDefs.StarterPackView
+
+/**
+ * Represents a step for retrieving a starter pack by its URI.
+ *
+ * @typeParam P - The parent type of this step, defaulting to {@link StepBuilder}.
+ * @typeParam C - The child context type, defaulting to `null`.
+ * @typeParam O - The output type, defaulting to {@link StepStarterPackOutput}.
+ *
+ * @example
+ * Get a specific starter pack:
+ * ```ts
+ * await Trotsky.init(agent)
+ * .starterPack("at://did:plc:example/app.bsky.graph.starterpack/packid")
+ * .tap((step) => {
+ * console.log(`Creator: ${step.context.creator.handle}`)
+ * console.log(`List: ${step.context.list?.name}`)
+ * console.log(`Joined this week: ${step.context.joinedWeekCount}`)
+ * })
+ * .run()
+ * ```
+ *
+ * @example
+ * Get starter pack details and display feeds:
+ * ```ts
+ * await Trotsky.init(agent)
+ * .starterPack("at://did:plc:example/app.bsky.graph.starterpack/packid")
+ * .tap((step) => {
+ * console.log(`Feeds in pack: ${step.context.feeds?.length || 0}`)
+ * step.context.feeds?.forEach(feed => {
+ * console.log(`- ${feed.displayName}`)
+ * })
+ * })
+ * .run()
+ * ```
+ *
+ * @public
+ */
+export class StepStarterPack extends Step
{
+
+ /**
+ * The URI of the starter pack to retrieve, which can be resolved dynamically at runtime.
+ * @internal
+ */
+ _uri: Resolvable
+
+ /**
+ * Initializes the StepStarterPack instance with the given agent, parent, and URI.
+ *
+ * @param agent - The AT protocol agent used for API calls.
+ * @param parent - The parent step in the chain.
+ * @param uri - The URI of the starter pack to retrieve, possibly resolvable at runtime.
+ */
+ constructor (agent: AtpAgent, parent: P, uri: Resolvable) {
+ super(agent, parent)
+ this._uri = uri
+ }
+
+ /**
+ * Clones the current step and returns a new instance with the same parameters.
+ * @param rest - Additional parameters to pass to the cloned step. This is useful for child class overriding the clone.
+ * @returns A new {@link StepStarterPack} instance.
+ */
+ override clone (...rest: unknown[]) {
+ return super.clone(this._uri, ...rest)
+ }
+
+ /**
+ * Applies the step logic to retrieve the starter pack and sets the output to the retrieved starter pack's data.
+ * If no starter pack is found, the output is set to `null`.
+ *
+ * @override
+ */
+ async apply () {
+ const { "data": { starterPack } } = await this.agent.app.bsky.graph.getStarterPack(await this.queryParams())
+ this.output = starterPack as O ?? null
+ }
+
+ /**
+ * Resolves the query parameters for retrieving the starter pack, including its URI.
+ *
+ * @returns A promise that resolves to the query parameters.
+ */
+ async queryParams (): Promise {
+ const uri = await resolveValue(this, this._uri)
+ const starterPack = uri.toString?.() ?? uri.toString()
+ return { starterPack }
+ }
+}
diff --git a/lib/core/StepStarterPacks.ts b/lib/core/StepStarterPacks.ts
new file mode 100644
index 0000000..9d61367
--- /dev/null
+++ b/lib/core/StepStarterPacks.ts
@@ -0,0 +1,126 @@
+import type { AtUri, AtpAgent, AppBskyGraphGetStarterPacks, AppBskyGraphDefs } from "@atproto/api"
+
+import { StepBuilderList, StepBuilderListIterator, StepStarterPacksEntry, type StepBuilder } from "../trotsky"
+import { Resolvable, resolveValue } from "./utils/resolvable"
+
+/**
+ * Type for the parameter passed to the {@link StepStarterPacks} class.
+ * Represents the URIs of several starter packs.
+ * @public
+ */
+export type StepStarterPacksUris = (string | AtUri)[]
+
+/**
+ * Defines the query parameters for retrieving multiple starter packs.
+ * @public
+ */
+export type StepStarterPacksQueryParams = AppBskyGraphGetStarterPacks.QueryParams
+
+/**
+ * Represents the output of a starter packs step, consisting of an array of starter pack views.
+ * @public
+ */
+export type StepStarterPacksOutput = AppBskyGraphDefs.StarterPackViewBasic[]
+
+/**
+ * Abstract class representing a list of starter packs to process.
+ *
+ * @typeParam P - The parent type of this step, defaulting to {@link StepBuilder}.
+ * @typeParam C - The child context type, defaulting to `null`.
+ * @typeParam O - The output type, defaulting to {@link StepStarterPacksOutput}.
+ *
+ * @example
+ * Get multiple starter packs by their URIs:
+ * ```ts
+ * const packUris = [
+ * "at://did:plc:example1/app.bsky.graph.starterpack/pack1",
+ * "at://did:plc:example2/app.bsky.graph.starterpack/pack2"
+ * ]
+ *
+ * await Trotsky.init(agent)
+ * .starterPacks(packUris)
+ * .tap((step) => {
+ * step.context.forEach(pack => {
+ * console.log(`${pack.creator.handle}: ${pack.listItemCount} members`)
+ * })
+ * })
+ * .run()
+ * ```
+ *
+ * @example
+ * Iterate through multiple starter packs:
+ * ```ts
+ * await Trotsky.init(agent)
+ * .starterPacks([uri1, uri2, uri3])
+ * .each()
+ * .tap((step) => {
+ * const pack = step.context
+ * console.log(`Pack by @${pack.creator.handle}`)
+ * console.log(`Joined: ${pack.joinedAllTimeCount || 0}`)
+ * })
+ * .run()
+ * ```
+ *
+ * @public
+ */
+export class StepStarterPacks extends StepBuilderList
{
+
+ /**
+ * Holds the list of steps to be executed for each starter pack entry.
+ */
+ _steps: StepStarterPacksEntry[] = []
+
+ /**
+ * The URIs of the starter packs to retrieve, which can be resolved dynamically at runtime.
+ */
+ _uris: Resolvable
+
+ /**
+ * Initializes the StepStarterPacks instance with the given agent, parent, and URIs.
+ *
+ * @param agent - The AT protocol agent used for API calls.
+ * @param parent - The parent step in the chain.
+ * @param uris - The URIs of the starter packs to retrieve, possibly resolvable at runtime.
+ */
+ constructor (agent: AtpAgent, parent: P, uris: Resolvable = []) {
+ super(agent, parent)
+ this._uris = uris
+ }
+
+ /**
+ * Clones the current step and returns a new instance with the same parameters.
+ * @param rest - Additional parameters to pass to the cloned step. This is useful for child class overriding the clone.
+ * @returns A new {@link StepStarterPacks} instance.
+ */
+ override clone (...rest: unknown[]) {
+ return super.clone(this._uris, ...rest)
+ }
+
+ /**
+ * Appends a new starter pack entry step to the current list and returns it.
+ *
+ * @param iterator - Optional iterator function to be executed for each item in the list.
+ * @returns The newly appended {@link StepStarterPacksEntry} instance.
+ */
+ each (iterator?: StepBuilderListIterator): StepStarterPacksEntry {
+ return this.withIterator(iterator).append(StepStarterPacksEntry)
+ }
+
+ /**
+ * Applies the step by resolving the URIs parameter and fetching the starter packs.
+ * Sets the starter packs data as the output of this step.
+ * @returns A promise that resolves when the step is complete.
+ */
+ async applyPagination (): Promise {
+ const uris = (await resolveValue(this, this._uris)).map(uri => uri.toString())
+
+ // If no URIs provided, return empty array
+ if (uris.length === 0) {
+ this.output = [] as unknown as O
+ return
+ }
+
+ const { data } = await this.agent.app.bsky.graph.getStarterPacks({ uris })
+ this.output = data.starterPacks as O
+ }
+}
diff --git a/lib/core/StepStarterPacksEntry.ts b/lib/core/StepStarterPacksEntry.ts
new file mode 100644
index 0000000..17d841a
--- /dev/null
+++ b/lib/core/StepStarterPacksEntry.ts
@@ -0,0 +1,52 @@
+import type { StepStarterPacks, StepStarterPacksOutput } from "../trotsky"
+import { StepBuilderListEntry } from "./StepBuilderListEntry"
+
+/**
+ * Represents an individual entry step within a {@link StepStarterPacks} list.
+ * Provides context for each starter pack in the iteration.
+ *
+ * @typeParam P - The parent type of this step, defaulting to {@link StepStarterPacks}.
+ * @typeParam C - The context type, defaulting to an element of {@link StepStarterPacksOutput}.
+ * @typeParam O - The output type, defaulting to `unknown`.
+ *
+ * @example
+ * Iterate through starter packs and log details:
+ * ```ts
+ * await Trotsky.init(agent)
+ * .starterPacks([uri1, uri2])
+ * .each()
+ * .tap((step) => {
+ * console.log(`Creator: @${step.context.creator.handle}`)
+ * console.log(`Members: ${step.context.listItemCount}`)
+ * })
+ * .run()
+ * ```
+ *
+ * @example
+ * Filter starter packs by join count:
+ * ```ts
+ * await Trotsky.init(agent)
+ * .starterPacks(packUris)
+ * .each()
+ * .when((step) => (step.context.joinedAllTimeCount || 0) > 100)
+ * .tap((step) => {
+ * console.log(`Popular pack: ${step.context.uri}`)
+ * })
+ * .run()
+ * ```
+ *
+ * @public
+ */
+export class StepStarterPacksEntry<
+ P = StepStarterPacks,
+ C = StepStarterPacksOutput[number],
+ O = unknown
+> extends StepBuilderListEntry {
+
+ /**
+ * Applies the step's logic but does nothing by default. This method is
+ * usually overridden by child classes but will not throw an error if not.
+ * @override
+ */
+ async apply (): Promise { }
+}
diff --git a/lib/core/Trotsky.ts b/lib/core/Trotsky.ts
index e05629b..c38dec6 100644
--- a/lib/core/Trotsky.ts
+++ b/lib/core/Trotsky.ts
@@ -8,19 +8,25 @@ import type { StepCreatePostParams } from "./StepCreatePost"
import type { StepWhenPredicate } from "./StepWhen"
import type { StepTapInterceptor } from "./StepTap"
import type { Resolvable } from "./utils/resolvable"
+import type { StepStarterPackUri } from "./StepStarterPack"
+import type { StepStarterPacksUris } from "./StepStarterPacks"
+import type { StepSearchStarterPacksQueryParams } from "./StepSearchStarterPacks"
-import {
- StepActor,
+import {
+ StepActor,
StepActors,
- StepWait,
+ StepWait,
StepPost,
- StepCreatePost,
- StepList,
+ StepCreatePost,
+ StepList,
StepListUri,
- StepSearchPosts,
- StepStreamPosts,
- StepTap,
- StepWhen,
+ StepStarterPack,
+ StepStarterPacks,
+ StepSearchPosts,
+ StepSearchStarterPacks,
+ StepStreamPosts,
+ StepTap,
+ StepWhen,
StepBuilder,
StepActorsParam,
StepPosts,
@@ -91,6 +97,24 @@ export class Trotsky extends StepBuilder {
return this.append(StepList, uri)
}
+ /**
+ * Adds a {@link StepStarterPack} step.
+ * @param uri - The starter pack URI.
+ * @returns The new {@link StepStarterPack} instance.
+ */
+ starterPack (uri: Resolvable) {
+ return this.append(StepStarterPack, uri)
+ }
+
+ /**
+ * Adds a {@link StepStarterPacks} step.
+ * @param uris - The starter pack URIs.
+ * @returns The new {@link StepStarterPacks} instance.
+ */
+ starterPacks (uris: Resolvable): StepStarterPacks {
+ return this.append(StepStarterPacks, uris)
+ }
+
/**
* Adds a {@link StepSearchPosts} step.
* @param queryParams - Search parameters.
@@ -100,6 +124,15 @@ export class Trotsky extends StepBuilder {
return this.append(StepSearchPosts, queryParams)
}
+ /**
+ * Adds a {@link StepSearchStarterPacks} step.
+ * @param queryParams - Search parameters.
+ * @returns The new {@link StepSearchStarterPacks} instance.
+ */
+ searchStarterPacks (queryParams: StepSearchStarterPacksQueryParams): StepSearchStarterPacks {
+ return this.append(StepSearchStarterPacks, queryParams)
+ }
+
/**
* Adds a {@link StepStreamPosts} step.
* @returns The new {@link StepStreamPosts} instance.
diff --git a/lib/core/mixins/ActorMixins.ts b/lib/core/mixins/ActorMixins.ts
index b8adc1e..cd1bf71 100644
--- a/lib/core/mixins/ActorMixins.ts
+++ b/lib/core/mixins/ActorMixins.ts
@@ -8,6 +8,7 @@ import {
StepActorLikes,
StepActorMute,
StepActorPosts,
+ StepActorStarterPacks,
StepActorUnblock,
StepActorUnfollow,
StepActorUnmute,
@@ -64,16 +65,25 @@ export abstract class ActorMixins extends Step
{
/**
* Appends a step to fetch the posts of the current actor.
- *
+ *
* @returns The appended {@link StepActorPosts} instance.
*/
posts (): StepActorPosts {
return this.append(StepActorPosts)
}
+ /**
+ * Appends a step to fetch the starter packs of the current actor.
+ *
+ * @returns The appended {@link StepActorStarterPacks} instance.
+ */
+ starterPacks (): StepActorStarterPacks {
+ return this.append(StepActorStarterPacks)
+ }
+
/**
* Appends a step to stream the posts of the current actor.
- *
+ *
* @typeParam T - The type of the stream posts step, defaulting to {@link StepActorStreamPosts}.
* @returns The appended {@link StepActorStreamPosts} instance.
*/
diff --git a/lib/core/utils/jetstream.ts b/lib/core/utils/jetstream.ts
index 21f927d..8a7e848 100644
--- a/lib/core/utils/jetstream.ts
+++ b/lib/core/utils/jetstream.ts
@@ -1,4 +1,4 @@
-import type { Did, FollowRecord, LikeRecord, PostRecord } from "@atproto/api"
+import type { Did, AppBskyGraphFollowRecord, AppBskyFeedLikeRecord, AppBskyFeedPostRecord } from "@atproto/api"
import WebSocket from "ws"
import EventEmitter from "events"
import { decompress } from "@skhaz/zstd"
@@ -24,7 +24,7 @@ export interface JetstreamMessageCommit extends JetstreamMessageBase {
"collection": string;
"rkey": string;
"cid": string;
- "record"?: Partial | Partial | Partial;
+ "record"?: Partial | Partial | Partial;
};
}
diff --git a/lib/trotsky.ts b/lib/trotsky.ts
index 03f0e44..d6367d2 100644
--- a/lib/trotsky.ts
+++ b/lib/trotsky.ts
@@ -80,6 +80,7 @@ export * from "./core/StepActorLikes"
export * from "./core/StepActorLists"
export * from "./core/StepActorMute"
export * from "./core/StepActorPosts"
+export * from "./core/StepActorStarterPacks"
export * from "./core/StepActorStreamPosts"
export * from "./core/StepActorUnblock"
export * from "./core/StepActorUnfollow"
@@ -97,6 +98,14 @@ export * from "./core/StepPostRepost"
export * from "./core/StepList"
export * from "./core/StepListMembers"
+// Single starter pack
+export * from "./core/StepStarterPack"
+
+// List of starter packs
+export * from "./core/StepStarterPacks"
+export * from "./core/StepStarterPacksEntry"
+export * from "./core/StepSearchStarterPacks"
+
// Utils
export * from "./core/StepTap"
export * from "./core/StepWait"
diff --git a/package.json b/package.json
index a45abb2..ecd9081 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "trotsky",
- "version": "0.1.9",
+ "version": "0.2.1",
"main": "dist/trotsky.js",
"typings": "dist/trotsky.d.ts",
"author": "hello@pirhoo.com",
diff --git a/tests/core/StepActorStarterPacks.test.ts b/tests/core/StepActorStarterPacks.test.ts
new file mode 100644
index 0000000..8f4107d
--- /dev/null
+++ b/tests/core/StepActorStarterPacks.test.ts
@@ -0,0 +1,178 @@
+import { afterAll, beforeAll, describe, expect, test } from "@jest/globals"
+import { TestNetwork, SeedClient, usersSeed } from "@atproto/dev-env"
+import { AtpAgent } from "@atproto/api"
+import { Trotsky, StepActorStarterPacks } from "../../lib/trotsky"
+
+describe("StepActorStarterPacks", () => {
+ let network: TestNetwork
+ let agent: AtpAgent
+ let sc: SeedClient
+
+ // accounts
+ let bob: { "did": string; "handle": string; "password": string }
+ let alice: { "did": string; "handle": string; "password": string }
+ let carol: { "did": string; "handle": string; "password": string }
+
+ // starter packs
+ let starterPack1: { "uri": string; "cid": string }
+ let starterPack2: { "uri": string; "cid": string }
+
+ beforeAll(async () => {
+ network = await TestNetwork.create({
+ "dbPostgresSchema": "trotsky_step_actor_starter_packs"
+ })
+
+ agent = network.bsky.getClient()
+ sc = network.getSeedClient()
+
+ // Seed users
+ await usersSeed(sc)
+ bob = sc.accounts[sc.dids.bob]
+ alice = sc.accounts[sc.dids.alice]
+ carol = sc.accounts[sc.dids.carol]
+
+ // Create starter packs for bob
+ starterPack1 = await sc.createStarterPack(bob.did, "Tech Community", [alice.did])
+ starterPack2 = await sc.createStarterPack(bob.did, "JavaScript Enthusiasts", [carol.did])
+
+ // Create a starter pack for alice (to test filtering)
+ await sc.createStarterPack(alice.did, "Alice's Pack", [bob.did])
+
+ await network.processAll()
+ }, 120e3)
+
+ afterAll(async () => {
+ await network.close()
+ })
+
+ test("should clone properly", () => {
+ const step = Trotsky.init(agent).actor(bob.handle).starterPacks()
+ const cloned = step.clone()
+ expect(cloned).toBeInstanceOf(StepActorStarterPacks)
+ })
+
+ test("should get actor's starter packs", async () => {
+ const packs = await Trotsky.init(agent)
+ .actor(bob.handle)
+ .starterPacks()
+ .runHere()
+
+ expect(packs).toBeInstanceOf(StepActorStarterPacks)
+ expect(packs.output).toBeInstanceOf(Array)
+ expect(packs.output.length).toBe(2)
+ })
+
+ test("should return starter packs with correct structure", async () => {
+ const packs = await Trotsky.init(agent)
+ .actor(bob.handle)
+ .starterPacks()
+ .runHere()
+
+ packs.output.forEach(pack => {
+ expect(pack).toHaveProperty("uri")
+ expect(pack).toHaveProperty("creator")
+ expect(pack.creator).toHaveProperty("handle")
+ expect(pack.creator).toHaveProperty("did")
+ expect(pack.creator.did).toBe(bob.did)
+ })
+ })
+
+ test("should verify URIs match created packs", async () => {
+ const packs = await Trotsky.init(agent)
+ .actor(bob.handle)
+ .starterPacks()
+ .runHere()
+
+ const uris = packs.output.map(p => p.uri)
+ expect(uris).toContain(starterPack1.uri.toString())
+ expect(uris).toContain(starterPack2.uri.toString())
+ })
+
+ test("should iterate through each starter pack", async () => {
+ const uris: string[] = []
+
+ await Trotsky.init(agent)
+ .actor(bob.handle)
+ .starterPacks()
+ .each()
+ .tap((step) => {
+ if (step?.context?.uri) {
+ uris.push(step.context.uri)
+ }
+ })
+ .run()
+
+ expect(uris).toHaveLength(2)
+ expect(uris).toContain(starterPack1.uri.toString())
+ expect(uris).toContain(starterPack2.uri.toString())
+ })
+
+ test("should filter starter packs with when()", async () => {
+ const filteredPacks: string[] = []
+
+ await Trotsky.init(agent)
+ .actor(bob.handle)
+ .starterPacks()
+ .each()
+ .when((step) => step?.context?.uri === starterPack1.uri.toString())
+ .tap((step) => {
+ if (step?.context?.uri) {
+ filteredPacks.push(step.context.uri)
+ }
+ })
+ .run()
+
+ expect(filteredPacks).toHaveLength(1)
+ expect(filteredPacks[0]).toBe(starterPack1.uri.toString())
+ })
+
+ test("should handle pagination with take()", async () => {
+ const packs = await Trotsky.init(agent)
+ .actor(bob.handle)
+ .starterPacks()
+ .take(1)
+ .runHere()
+
+ expect(packs.output.length).toBeLessThanOrEqual(1)
+ })
+
+ test("should return empty array for actor with no packs", async () => {
+ const packs = await Trotsky.init(agent)
+ .actor(carol.handle)
+ .starterPacks()
+ .runHere()
+
+ expect(packs.output).toBeInstanceOf(Array)
+ expect(packs.output).toHaveLength(0)
+ })
+
+ test("should return indexed timestamps", async () => {
+ const packs = await Trotsky.init(agent)
+ .actor(bob.handle)
+ .starterPacks()
+ .runHere()
+
+ packs.output.forEach(pack => {
+ expect(pack).toHaveProperty("indexedAt")
+ expect(pack.indexedAt).toBeTruthy()
+ })
+ })
+
+ test("should work with tap() to process results", async () => {
+ const result = await Trotsky.init(agent)
+ .actor(bob.handle)
+ .starterPacks()
+ .runHere()
+
+ expect(result.output).toBeInstanceOf(Array)
+ expect(result.output.length).toBeGreaterThan(0)
+ })
+
+ test("should throw error when no context is available", async () => {
+ const step = new StepActorStarterPacks(agent, null)
+
+ await expect(async () => {
+ await step.applyPagination()
+ }).rejects.toThrow("No context found for StepActorStarterPacks")
+ })
+})
diff --git a/tests/core/StepSearchStarterPacks.test.ts b/tests/core/StepSearchStarterPacks.test.ts
new file mode 100644
index 0000000..ddf091f
--- /dev/null
+++ b/tests/core/StepSearchStarterPacks.test.ts
@@ -0,0 +1,158 @@
+import { afterAll, beforeAll, describe, expect, test } from "@jest/globals"
+import { TestNetwork, SeedClient, usersSeed } from "@atproto/dev-env"
+import { AtpAgent } from "@atproto/api"
+import { Trotsky, StepSearchStarterPacks } from "../../lib/trotsky"
+
+describe("StepSearchStarterPacks", () => {
+ let network: TestNetwork
+ let agent: AtpAgent
+ let sc: SeedClient
+
+ // accounts
+ let bob: { "did": string; "handle": string; "password": string }
+ let alice: { "did": string; "handle": string; "password": string }
+ let carol: { "did": string; "handle": string; "password": string }
+
+ beforeAll(async () => {
+ network = await TestNetwork.create({
+ "dbPostgresSchema": "trotsky_step_search_starter_packs"
+ })
+
+ agent = network.bsky.getClient()
+ sc = network.getSeedClient()
+
+ // Seed users
+ await usersSeed(sc)
+ bob = sc.accounts[sc.dids.bob]
+ alice = sc.accounts[sc.dids.alice]
+ carol = sc.accounts[sc.dids.carol]
+
+ // Create starter packs with different names for search testing
+ await sc.createStarterPack(bob.did, "Tech Community", [alice.did])
+ await sc.createStarterPack(bob.did, "TypeScript Developers", [carol.did])
+ await sc.createStarterPack(alice.did, "JavaScript Enthusiasts", [bob.did])
+
+ await network.processAll()
+ }, 120e3)
+
+ afterAll(async () => {
+ await network.close()
+ })
+
+ test("should clone properly", () => {
+ const step = Trotsky.init(agent).searchStarterPacks({ "q": "tech" })
+ const cloned = step.clone()
+ expect(cloned).toBeInstanceOf(StepSearchStarterPacks)
+ expect(cloned._queryParams).toEqual(step._queryParams)
+ })
+
+ test("should search starter packs by query", async () => {
+ const packs = await Trotsky.init(agent)
+ .searchStarterPacks({ "q": "Tech" })
+ .runHere()
+
+ expect(packs).toBeInstanceOf(StepSearchStarterPacks)
+ expect(packs.output).toBeInstanceOf(Array)
+ expect(packs.output.length).toBeGreaterThan(0)
+ })
+
+ test("should return starter packs with correct structure", async () => {
+ const packs = await Trotsky.init(agent)
+ .searchStarterPacks({ "q": "Community" })
+ .runHere()
+
+ packs.output.forEach(pack => {
+ expect(pack).toHaveProperty("uri")
+ expect(pack).toHaveProperty("creator")
+ expect(pack.creator).toHaveProperty("handle")
+ expect(pack.creator).toHaveProperty("did")
+ })
+ })
+
+ test("should support limit parameter", async () => {
+ const packsWithoutLimit = await Trotsky.init(agent)
+ .searchStarterPacks({ "q": "Tech" })
+ .runHere()
+
+ const packsWithLimit = await Trotsky.init(agent)
+ .searchStarterPacks({ "q": "Tech", "limit": 1 })
+ .runHere()
+
+ // With limit should return same or fewer results
+ expect(packsWithLimit.output.length).toBeLessThanOrEqual(packsWithoutLimit.output.length)
+ })
+
+ test("should iterate through search results with each()", async () => {
+ const uris: string[] = []
+
+ await Trotsky.init(agent)
+ .searchStarterPacks({ "q": "JavaScript" })
+ .take(5)
+ .each()
+ .tap((step) => {
+ if (step?.context?.uri) {
+ uris.push(step.context.uri)
+ }
+ })
+ .run()
+
+ expect(uris.length).toBeGreaterThan(0)
+ })
+
+ test("should filter search results with when()", async () => {
+ const filteredPacks: string[] = []
+
+ await Trotsky.init(agent)
+ .searchStarterPacks({ "q": "TypeScript" })
+ .take(10)
+ .each()
+ .when((step) => step?.context?.creator.handle === bob.handle)
+ .tap((step) => {
+ if (step?.context?.uri) {
+ filteredPacks.push(step.context.uri)
+ }
+ })
+ .run()
+
+ // Should only include packs created by bob
+ expect(filteredPacks.length).toBeGreaterThan(0)
+ })
+
+ test("should handle pagination with take()", async () => {
+ const packs = await Trotsky.init(agent)
+ .searchStarterPacks({ "q": "Developers" })
+ .take(2)
+ .runHere()
+
+ expect(packs.output.length).toBeLessThanOrEqual(2)
+ })
+
+ test("should return empty array for query with no results", async () => {
+ const packs = await Trotsky.init(agent)
+ .searchStarterPacks({ "q": "NonExistentQueryThatWillNeverMatch12345" })
+ .runHere()
+
+ expect(packs.output).toBeInstanceOf(Array)
+ expect(packs.output).toHaveLength(0)
+ })
+
+ test("should return starter packs with indexed timestamps", async () => {
+ const packs = await Trotsky.init(agent)
+ .searchStarterPacks({ "q": "Community" })
+ .runHere()
+
+ packs.output.forEach(pack => {
+ expect(pack).toHaveProperty("indexedAt")
+ expect(pack.indexedAt).toBeTruthy()
+ })
+ })
+
+ test("should work with tap() to process results", async () => {
+ const result = await Trotsky.init(agent)
+ .searchStarterPacks({ "q": "Tech" })
+ .runHere()
+
+ expect(result.output).toBeInstanceOf(Array)
+ expect(result.output.length).toBeGreaterThan(0)
+ })
+})
diff --git a/tests/core/StepStarterPack.test.ts b/tests/core/StepStarterPack.test.ts
new file mode 100644
index 0000000..c9fda41
--- /dev/null
+++ b/tests/core/StepStarterPack.test.ts
@@ -0,0 +1,93 @@
+import { afterAll, beforeAll, describe, expect, test } from "@jest/globals"
+import { AtpAgent } from "@atproto/api"
+import { TestNetwork, SeedClient, usersSeed, RecordRef } from "@atproto/dev-env"
+
+import { StepStarterPack, Trotsky } from "../../lib/trotsky"
+
+describe("StepStarterPack", () => {
+ let network: TestNetwork
+ let agent: AtpAgent
+ let sc: SeedClient
+ let alice: { "did": string; "handle": string; "password": string }
+ let bob: { "did": string; "handle": string; "password": string }
+ let carol: { "did": string; "handle": string; "password": string }
+ let starterPack: RecordRef
+
+ beforeAll(async () => {
+ network = await TestNetwork.create({ "dbPostgresSchema": "step_starter_pack" })
+ agent = network.bsky.getClient()
+ sc = network.getSeedClient()
+ // Seed users
+ await usersSeed(sc)
+ bob = sc.accounts[sc.dids.bob]
+ alice = sc.accounts[sc.dids.alice]
+ carol = sc.accounts[sc.dids.carol]
+ // Create a starter pack with Alice and Carol as members
+ starterPack = await sc.createStarterPack(bob.did, "Bob's Starter Pack", [alice.did, carol.did])
+ await network.processAll()
+ })
+
+ afterAll(async () => {
+ // For some reason the AppView schema is not being dropped
+ await network.bsky.db.db.schema.dropSchema("appview_step_starter_pack").cascade().execute()
+ await network.close()
+ })
+
+ test("clones it", () => {
+ const clone = new StepStarterPack(agent, null, "at://did/repo/rkey").clone()
+ expect(clone).toBeInstanceOf(StepStarterPack)
+ expect(clone).toHaveProperty("_uri", "at://did/repo/rkey")
+ })
+
+ test("should get a starter pack", async () => {
+ await expect(Trotsky.init(agent).starterPack(starterPack.uri).run()).resolves.not.toThrow()
+ })
+
+ test("should get a starter pack's creator", async () => {
+ const pack = await Trotsky.init(agent).starterPack(starterPack.uri).runHere()
+ expect(pack.output).toHaveProperty("creator")
+ expect(pack.output.creator).toHaveProperty("handle", bob.handle)
+ })
+
+ test("should get a starter pack's URI", async () => {
+ const pack = await Trotsky.init(agent).starterPack(starterPack.uri).runHere()
+ expect(pack.output).toHaveProperty("uri")
+ expect(pack.output.uri).toBe(starterPack.uri.toString())
+ })
+
+ test("should get a starter pack with list items", async () => {
+ const pack = await Trotsky.init(agent).starterPack(starterPack.uri).runHere()
+ expect(pack.output).toHaveProperty("listItemsSample")
+ expect(pack.output.listItemsSample).toBeInstanceOf(Array)
+ })
+
+ test("should have list items sample with expected members", async () => {
+ const pack = await Trotsky.init(agent).starterPack(starterPack.uri).runHere()
+ const handles = pack.output.listItemsSample?.map(item => item.subject.handle) || []
+ expect(handles).toContain(alice.handle)
+ expect(handles).toContain(carol.handle)
+ })
+
+ test("should get a starter pack with indexed timestamp", async () => {
+ const pack = await Trotsky.init(agent).starterPack(starterPack.uri).runHere()
+ expect(pack.output).toHaveProperty("indexedAt")
+ expect(typeof pack.output.indexedAt).toBe("string")
+ })
+
+ test("should get a starter pack with CID", async () => {
+ const pack = await Trotsky.init(agent).starterPack(starterPack.uri).runHere()
+ expect(pack.output).toHaveProperty("cid")
+ expect(typeof pack.output.cid).toBe("string")
+ })
+
+ test("should get a starter pack with record", async () => {
+ const pack = await Trotsky.init(agent).starterPack(starterPack.uri).runHere()
+ expect(pack.output).toHaveProperty("record")
+ expect(typeof pack.output.record).toBe("object")
+ })
+
+ test("should handle non-existent starter pack gracefully", async () => {
+ const fakeUri = "at://did:plc:fake/app.bsky.graph.starterpack/fake"
+ await expect(Trotsky.init(agent).starterPack(fakeUri).run()).rejects.toThrow()
+ })
+})
diff --git a/tests/core/StepStarterPacks.test.ts b/tests/core/StepStarterPacks.test.ts
new file mode 100644
index 0000000..7e597f4
--- /dev/null
+++ b/tests/core/StepStarterPacks.test.ts
@@ -0,0 +1,163 @@
+import { afterAll, beforeAll, describe, expect, test } from "@jest/globals"
+import { TestNetwork, SeedClient, usersSeed, RecordRef } from "@atproto/dev-env"
+import { AtpAgent } from "@atproto/api"
+import { Trotsky } from "../../lib/trotsky"
+
+describe("StepStarterPacks", () => {
+ let network: TestNetwork
+ let agent: AtpAgent
+ let sc: SeedClient
+
+ // accounts
+ let bob: { "did": string; "handle": string; "password": string }
+ let alice: { "did": string; "handle": string; "password": string }
+ let carol: { "did": string; "handle": string; "password": string }
+ let dan: { "did": string; "handle": string; "password": string }
+
+ // starter packs
+ let starterPack1: RecordRef
+ let starterPack2: RecordRef
+
+ beforeAll(async () => {
+ network = await TestNetwork.create({
+ "dbPostgresSchema": "trotsky_step_starter_packs"
+ })
+
+ agent = network.bsky.getClient()
+ sc = network.getSeedClient()
+
+ // Seed users
+ await usersSeed(sc)
+ bob = sc.accounts[sc.dids.bob]
+ alice = sc.accounts[sc.dids.alice]
+ carol = sc.accounts[sc.dids.carol]
+ dan = sc.accounts[sc.dids.dan]
+
+ // Create first starter pack with alice and carol
+ starterPack1 = await sc.createStarterPack(bob.did, "Tech Community", [alice.did, carol.did])
+
+ // Create second starter pack with dan
+ starterPack2 = await sc.createStarterPack(bob.did, "Developers", [dan.did])
+
+ await network.processAll()
+ }, 120e3)
+
+ afterAll(async () => {
+ await network.close()
+ })
+
+ test("should clone properly", () => {
+ const step = Trotsky.init(agent).starterPacks([starterPack1.uri, starterPack2.uri])
+ const cloned = step.clone()
+ expect(cloned).toBeInstanceOf(step.constructor)
+ expect(cloned._uris).toEqual(step._uris)
+ })
+
+ test("should get multiple starter packs", async () => {
+ const packs = await Trotsky.init(agent)
+ .starterPacks([starterPack1.uri, starterPack2.uri])
+ .runHere()
+
+ expect(packs.output).toHaveLength(2)
+ expect(packs.output[0]).toHaveProperty("uri")
+ expect(packs.output[1]).toHaveProperty("uri")
+ })
+
+ test("should get starter packs with correct URIs", async () => {
+ const packs = await Trotsky.init(agent)
+ .starterPacks([starterPack1.uri, starterPack2.uri])
+ .runHere()
+
+ const uris = packs.output.map(pack => pack.uri)
+ expect(uris).toContain(starterPack1.uri.toString())
+ expect(uris).toContain(starterPack2.uri.toString())
+ })
+
+ test("should get starter packs with creator information", async () => {
+ const packs = await Trotsky.init(agent)
+ .starterPacks([starterPack1.uri, starterPack2.uri])
+ .runHere()
+
+ packs.output.forEach(pack => {
+ expect(pack).toHaveProperty("creator")
+ expect(pack.creator).toHaveProperty("handle", bob.handle)
+ expect(pack.creator).toHaveProperty("did", bob.did)
+ })
+ })
+
+ test("should get starter packs with list item counts", async () => {
+ const packs = await Trotsky.init(agent)
+ .starterPacks([starterPack1.uri, starterPack2.uri])
+ .runHere()
+
+ const pack1 = packs.output.find(p => p.uri === starterPack1.uri.toString())
+ const pack2 = packs.output.find(p => p.uri === starterPack2.uri.toString())
+
+ // listItemCount may be present if supported by the API
+ expect(pack1).toBeDefined()
+ expect(pack2).toBeDefined()
+ // The count may or may not be populated depending on the test environment
+ // If available, it should be a non-negative number
+ expect(pack1?.listItemCount === undefined || pack1.listItemCount >= 0).toBe(true)
+ expect(pack2?.listItemCount === undefined || pack2.listItemCount >= 0).toBe(true)
+ })
+
+ test("should iterate through each starter pack", async () => {
+ const uris: string[] = []
+
+ await Trotsky.init(agent)
+ .starterPacks([starterPack1.uri, starterPack2.uri])
+ .each()
+ .tap((step) => {
+ uris.push(step?.context?.uri || "")
+ })
+ .run()
+
+ expect(uris).toHaveLength(2)
+ expect(uris).toContain(starterPack1.uri.toString())
+ expect(uris).toContain(starterPack2.uri.toString())
+ })
+
+ test("should filter starter packs with when()", async () => {
+ const filteredPacks: string[] = []
+
+ await Trotsky.init(agent)
+ .starterPacks([starterPack1.uri, starterPack2.uri])
+ .each()
+ .when((step) => step?.context?.uri === starterPack1.uri.toString())
+ .tap((step) => {
+ filteredPacks.push(step?.context?.uri || "")
+ })
+ .run()
+
+ expect(filteredPacks).toHaveLength(1)
+ expect(filteredPacks[0]).toBe(starterPack1.uri.toString())
+ })
+
+ test("should handle empty array", async () => {
+ const packs = await Trotsky.init(agent).starterPacks([]).runHere()
+ expect(packs.output).toHaveLength(0)
+ })
+
+ test("should return indexed timestamps", async () => {
+ const packs = await Trotsky.init(agent)
+ .starterPacks([starterPack1.uri, starterPack2.uri])
+ .runHere()
+
+ packs.output.forEach(pack => {
+ expect(pack).toHaveProperty("indexedAt")
+ expect(pack.indexedAt).toBeTruthy()
+ })
+ })
+
+ test("should iterate with custom iterator function", async () => {
+ let count = 0
+
+ await Trotsky.init(agent)
+ .starterPacks([starterPack1.uri, starterPack2.uri])
+ .each(() => count++)
+ .run()
+
+ expect(count).toBe(2)
+ })
+})