Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1577,9 +1577,9 @@ export class AxonOps {
* @returns {Promise<Axon[]>} An array of {@link Axon} instances.
*/
async list(params?: AxonListParams, options?: Core.RequestOptions): Promise<Axon[]> {
const result = await this.client.axons.list(params, options);
const page = await this.client.axons.list(params, options);
const axons: Axon[] = [];
for await (const axon of result) {
for (const axon of page.getPaginatedItems()) {
Comment on lines +1580 to +1582
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: This change breaks the documented behavior of listing all items when no pagination limit is provided: it now always returns only the first page. Preserve full auto-pagination when limit is not explicitly set, and only use getPaginatedItems() for the single-page path when a limit is provided. [logic error]

Severity Level: Major ⚠️
- ❌ Axon listing without limit returns incomplete results.
- ⚠️ Existing code examples suggest full list is expected.
- ⚠️ Any bulk axon operations may silently skip items.
Suggested change
const page = await this.client.axons.list(params, options);
const axons: Axon[] = [];
for await (const axon of result) {
for (const axon of page.getPaginatedItems()) {
const axons: Axon[] = [];
if (params?.limit !== undefined) {
const page = await this.client.axons.list(params, options);
for (const axon of page.getPaginatedItems()) {
axons.push(Axon.fromId(this.client, axon.id));
}
} else {
for await (const axon of this.client.axons.list(params, options)) {
axons.push(Axon.fromId(this.client, axon.id));
}
}
Steps of Reproduction ✅
1. Import and instantiate the SDK client as shown in `src/sdk.ts` RunloopSDK class (e.g.,
`const runloop = new RunloopSDK();`).

2. Call the AxonOps list method without a limit via `runloop.axon.list()` (AxonOps is
defined in `src/sdk.ts` under the `[Beta] Axon SDK interface for managing axons` section,
and its `list` implementation uses `const page = await this.client.axons.list(params,
options);` followed by `for (const axon of page.getPaginatedItems()) { ... }` at lines
1580–1582 in the PR hunk).

3. Ensure the underlying API has more axons than fit on a single page (the API client
`this.client.axons.list` is a paginated endpoint as implied by `getPaginatedItems()` and
the PR description about auto-pagination).

4. Observe that `runloop.axon.list()` returns only the items from the first page because
it iterates `page.getPaginatedItems()` for a single page, whereas prior behavior
(described in the PR text and other list implementations like `AgentOps.list` and
`ScorerOps.list` in `src/sdk.ts`) auto-paginated across all pages when no `limit` was
provided.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/sdk.ts
**Line:** 1580:1582
**Comment:**
	*Logic Error: This change breaks the documented behavior of listing all items when no pagination limit is provided: it now always returns only the first page. Preserve full auto-pagination when `limit` is not explicitly set, and only use `getPaginatedItems()` for the single-page path when a `limit` is provided.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

axons.push(Axon.fromId(this.client, axon.id));
}
return axons;
Expand Down Expand Up @@ -1789,10 +1789,10 @@ export class NetworkPolicyOps {
* @returns {Promise<NetworkPolicy[]>} An array of {@link NetworkPolicy} instances.
*/
async list(params?: NetworkPolicyListParams, options?: Core.RequestOptions): Promise<NetworkPolicy[]> {
const result = await this.client.networkPolicies.list(params, options);
const page = await this.client.networkPolicies.list(params, options);
const policies: NetworkPolicy[] = [];

for await (const policy of result) {
for (const policy of page.getPaginatedItems()) {
policies.push(NetworkPolicy.fromId(this.client, policy.id));
}

Expand Down Expand Up @@ -1900,10 +1900,10 @@ export class GatewayConfigOps {
* @returns {Promise<GatewayConfig[]>} An array of {@link GatewayConfig} instances.
*/
async list(params?: GatewayConfigListParams, options?: Core.RequestOptions): Promise<GatewayConfig[]> {
const result = await this.client.gatewayConfigs.list(params, options);
const page = await this.client.gatewayConfigs.list(params, options);
const configs: GatewayConfig[] = [];

for await (const config of result) {
for (const config of page.getPaginatedItems()) {
configs.push(GatewayConfig.fromId(this.client, config.id));
}

Expand Down Expand Up @@ -2002,10 +2002,10 @@ export class McpConfigOps {
* @returns {Promise<McpConfig[]>} An array of {@link McpConfig} instances.
*/
async list(params?: McpConfigListParams, options?: Core.RequestOptions): Promise<McpConfig[]> {
const result = await this.client.mcpConfigs.list(params, options);
const page = await this.client.mcpConfigs.list(params, options);
const configs: McpConfig[] = [];

for await (const config of result) {
for (const config of page.getPaginatedItems()) {
configs.push(McpConfig.fromId(this.client, config.id));
}

Expand Down
4 changes: 2 additions & 2 deletions src/sdk/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ export class Agent {
params?: AgentListParams,
options?: Core.RequestOptions,
): Promise<Agent[]> {
const agents = await client.agents.list(params, options);
const page = await client.agents.list(params, options);
const result: Agent[] = [];

for await (const agent of agents) {
for (const agent of page.getPaginatedItems()) {
Comment on lines +119 to +122
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: This change breaks the previous list() contract by always returning only the first page, even when no limit is provided. Callers using runloop.agent.list() to fetch all agents will now silently get partial results. Keep first-page behavior only when limit is explicitly set, and preserve auto-pagination otherwise. [logic error]

Severity Level: Major ⚠️
- ❌ Object-oriented `Agent.list` no longer returns all agents.
- ⚠️ Callers expecting full auto-pagination get partial results.
- ⚠️ Downstream features assuming complete agent set may misbehave.
Suggested change
const page = await client.agents.list(params, options);
const result: Agent[] = [];
for await (const agent of agents) {
for (const agent of page.getPaginatedItems()) {
const result: Agent[] = [];
if (params?.limit !== undefined) {
const page = await client.agents.list(params, options);
for (const agent of page.getPaginatedItems()) {
result.push(new Agent(client, agent.id));
}
} else {
for await (const agent of client.agents.list(params, options)) {
result.push(new Agent(client, agent.id));
}
Steps of Reproduction ✅
1. In a consumer project using this SDK, call `runloop.agent.list()` **without** passing a
`limit` parameter, which routes to `Agent.list(client, params?, options?)` in
`src/sdk/agent.ts:114-127` (final file state).

2. Inside `Agent.list`, the code at `src/sdk/agent.ts:119` executes `const page = await
client.agents.list(params, options);`, obtaining only the first page of results from the
underlying `client.agents.list` paginator.

3. The subsequent loop at `src/sdk/agent.ts:122-123` iterates `for (const agent of
page.getPaginatedItems())`, which, per the PR description, returns only items from that
single page rather than auto-paginating through all pages.

4. The function returns `result` at `src/sdk/agent.ts:126`, containing only the first page
of agents; any existing caller that previously relied on `runloop.agent.list()` to
auto-paginate and return all agents now silently receives a truncated list instead of the
complete set.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/sdk/agent.ts
**Line:** 119:122
**Comment:**
	*Logic Error: This change breaks the previous `list()` contract by always returning only the first page, even when no `limit` is provided. Callers using `runloop.agent.list()` to fetch all agents will now silently get partial results. Keep first-page behavior only when `limit` is explicitly set, and preserve auto-pagination otherwise.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

result.push(new Agent(client, agent.id));
}

Expand Down
4 changes: 2 additions & 2 deletions src/sdk/scorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ export class Scorer {
params?: ScorerListParams,
options?: Core.RequestOptions,
): Promise<Scorer[]> {
const scorers = await client.scenarios.scorers.list(params, options);
const page = await client.scenarios.scorers.list(params, options);
const result: Scorer[] = [];

for await (const scorer of scorers) {
for (const scorer of page.getPaginatedItems()) {
result.push(Scorer.fromId(client, scorer.id));
}

Expand Down
4 changes: 2 additions & 2 deletions src/sdk/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ export class Snapshot {
params?: DevboxListDiskSnapshotsParams,
options?: Core.RequestOptions,
): Promise<Snapshot[]> {
const snapshots = await client.devboxes.listDiskSnapshots(params, options);
const page = await client.devboxes.listDiskSnapshots(params, options);
const result: Snapshot[] = [];

for await (const snapshot of snapshots) {
for (const snapshot of page.getPaginatedItems()) {
result.push(new Snapshot(client, snapshot.id));
}

Expand Down
4 changes: 2 additions & 2 deletions src/sdk/storage-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,10 @@ export class StorageObject {
params?: ObjectListParams,
options?: Core.RequestOptions,
): Promise<StorageObject[]> {
const objects = await client.objects.list(params, options);
const page = await client.objects.list(params, options);
const result: StorageObject[] = [];

for await (const obj of objects) {
for (const obj of page.getPaginatedItems()) {
result.push(new StorageObject(client, obj.id, null));
}

Expand Down
31 changes: 12 additions & 19 deletions tests/objects/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,16 +182,11 @@ describe('Agent (SDK)', () => {
},
];

// Mock async iterator
const asyncIterator = {
async *[Symbol.asyncIterator]() {
for (const agent of mockAgents) {
yield agent;
}
},
const mockPage = {
getPaginatedItems: () => mockAgents,
};

mockClient.agents.list.mockReturnValue(asyncIterator);
mockClient.agents.list.mockReturnValue(mockPage);

const agents = await Agent.list(mockClient);

Expand All @@ -204,33 +199,31 @@ describe('Agent (SDK)', () => {
});

it('should pass filter parameters to list', async () => {
const asyncIterator = {
async *[Symbol.asyncIterator]() {
yield {
const mockPage = {
getPaginatedItems: () => [
{
id: 'agent-001',
name: 'filtered-agent',
version: '1.0.0',
create_time_ms: Date.now(),
is_public: false,
};
},
},
],
};

mockClient.agents.list.mockReturnValue(asyncIterator);
mockClient.agents.list.mockReturnValue(mockPage);

await Agent.list(mockClient, { name: 'filtered-agent' });

expect(mockClient.agents.list).toHaveBeenCalledWith({ name: 'filtered-agent' }, undefined);
});

it('should handle empty list', async () => {
const asyncIterator = {
async *[Symbol.asyncIterator]() {
// Empty iterator
},
const mockPage = {
getPaginatedItems: () => [],
};

mockClient.agents.list.mockReturnValue(asyncIterator);
mockClient.agents.list.mockReturnValue(mockPage);

const agents = await Agent.list(mockClient);

Expand Down
26 changes: 9 additions & 17 deletions tests/objects/scorer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,11 @@ describe('Scorer', () => {
{ id: 'scs_003', type: 'third', bash_script: 'echo "0.0"' },
];

const asyncIterator = {
async *[Symbol.asyncIterator]() {
for (const scorer of mockScorers) {
yield scorer;
}
},
const mockPage = {
getPaginatedItems: () => mockScorers,
};

mockClient.scenarios.scorers.list.mockReturnValue(asyncIterator);
mockClient.scenarios.scorers.list.mockReturnValue(mockPage);

const scorers = await Scorer.list(mockClient);

Expand All @@ -87,27 +83,23 @@ describe('Scorer', () => {
});

it('should pass filter parameters to list', async () => {
const asyncIterator = {
async *[Symbol.asyncIterator]() {
yield { id: 'scs_001' };
},
const mockPage = {
getPaginatedItems: () => [{ id: 'scs_001' }],
};

mockClient.scenarios.scorers.list.mockReturnValue(asyncIterator);
mockClient.scenarios.scorers.list.mockReturnValue(mockPage);

await Scorer.list(mockClient, { limit: 10 });

expect(mockClient.scenarios.scorers.list).toHaveBeenCalledWith({ limit: 10 }, undefined);
});

it('should handle empty list', async () => {
const asyncIterator = {
async *[Symbol.asyncIterator]() {
// Empty iterator
},
const mockPage = {
getPaginatedItems: () => [],
};

mockClient.scenarios.scorers.list.mockReturnValue(asyncIterator);
mockClient.scenarios.scorers.list.mockReturnValue(mockPage);

const scorers = await Scorer.list(mockClient);

Expand Down
13 changes: 3 additions & 10 deletions tests/objects/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,7 @@ describe('Snapshot (New API)', () => {
};

const mockPage = {
[Symbol.asyncIterator]: async function* () {
yield snapshot1;
yield snapshot2;
},
getPaginatedItems: () => [snapshot1, snapshot2],
};

mockClient.devboxes.listDiskSnapshots.mockResolvedValue(mockPage as any);
Expand All @@ -107,9 +104,7 @@ describe('Snapshot (New API)', () => {

it('should filter by devbox ID', async () => {
const mockPage = {
[Symbol.asyncIterator]: async function* () {
yield mockSnapshotData;
},
getPaginatedItems: () => [mockSnapshotData],
};

mockClient.devboxes.listDiskSnapshots.mockResolvedValue(mockPage as any);
Expand All @@ -124,9 +119,7 @@ describe('Snapshot (New API)', () => {

it('should return empty array when no snapshots found', async () => {
const mockPage = {
[Symbol.asyncIterator]: async function* () {
// Empty iterator
},
getPaginatedItems: () => [],
};

mockClient.devboxes.listDiskSnapshots.mockResolvedValue(mockPage as any);
Expand Down
9 changes: 2 additions & 7 deletions tests/objects/storage-object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,7 @@ describe('StorageObject (New API)', () => {
};

const mockPage = {
[Symbol.asyncIterator]: async function* () {
yield obj1;
yield obj2;
},
getPaginatedItems: () => [obj1, obj2],
};

mockClient.objects.list.mockResolvedValue(mockPage as any);
Expand All @@ -213,9 +210,7 @@ describe('StorageObject (New API)', () => {

it('should support filtering', async () => {
const mockPage = {
[Symbol.asyncIterator]: async function* () {
yield mockObjectData;
},
getPaginatedItems: () => [mockObjectData],
};

mockClient.objects.list.mockResolvedValue(mockPage as any);
Expand Down
4 changes: 1 addition & 3 deletions tests/sdk/axon-ops.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ describe('AxonOps', () => {
describe('list', () => {
function mockPageResult(items: AxonView[]) {
return {
[Symbol.asyncIterator]: async function* () {
yield* items;
},
getPaginatedItems: () => items,
};
}

Expand Down
Loading